@hyperframes/producer 0.8.186 → 0.8.189
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 +1301 -1329
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +65 -64
- package/dist/index.js +1673 -1668
- package/dist/public-server.js +1589 -1567
- package/dist/server.d.ts +1 -0
- package/dist/services/distributed/assemble.d.ts +2 -1
- package/dist/services/distributed/chunkFrames.d.ts +1 -1
- package/dist/services/distributed/plan.d.ts +2 -1
- package/dist/services/render/segmentManifest.d.ts +1 -0
- package/dist/services/render/stages/assembleStage.d.ts +1 -0
- package/dist/services/render/stages/audioStage.d.ts +6 -4
- package/dist/services/render/stages/captureStage.d.ts +3 -8
- package/dist/services/render/stages/freezePlan.d.ts +2 -1
- package/package.json +7 -7
package/dist/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
|
|
@@ -3444,7 +3444,9 @@ async function discoverMediaFromBrowser(page) {
|
|
|
3444
3444
|
const tagName = isImage ? "image" : htmlEl.tagName.toLowerCase() === "video" ? "video" : "audio";
|
|
3445
3445
|
const id = htmlEl.getAttribute("data-hf-render-id") || htmlEl.id || (isImage ? autoImageIds.get(htmlEl) : void 0);
|
|
3446
3446
|
if (!id) return;
|
|
3447
|
-
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 || "";
|
|
3448
3450
|
const start = parseFloat(htmlEl.getAttribute("data-start") || "0");
|
|
3449
3451
|
const endRaw = htmlEl.getAttribute("data-end");
|
|
3450
3452
|
const durationRaw = htmlEl.getAttribute("data-duration");
|
|
@@ -4947,1241 +4949,1225 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
4947
4949
|
}
|
|
4948
4950
|
|
|
4949
4951
|
// src/services/render/stages/audioStage.ts
|
|
4950
|
-
import { join as
|
|
4952
|
+
import { join as join10 } from "node:path";
|
|
4953
|
+
import { rmSync as rmSync5 } from "node:fs";
|
|
4951
4954
|
import {
|
|
4952
4955
|
MIXED_AUDIO_FILENAME,
|
|
4953
4956
|
audioFailureDiagnostics,
|
|
4954
4957
|
processCompositionAudio
|
|
4955
4958
|
} from "@hyperframes/engine";
|
|
4956
|
-
|
|
4957
|
-
|
|
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;
|
|
4958
4989
|
}
|
|
4959
|
-
async function
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
assertNotAborted,
|
|
4973
|
-
log
|
|
4974
|
-
} = input;
|
|
4975
|
-
const stage3Start = Date.now();
|
|
4976
|
-
const audioOutputPath = join9(workDir, MIXED_AUDIO_FILENAME);
|
|
4977
|
-
let hasAudio = false;
|
|
4978
|
-
let audioError;
|
|
4979
|
-
let audioFailures;
|
|
4980
|
-
if (audios.length > 0) {
|
|
4981
|
-
let audioResult;
|
|
4982
|
-
try {
|
|
4983
|
-
audioResult = await processCompositionAudio(
|
|
4984
|
-
audios,
|
|
4985
|
-
projectDir,
|
|
4986
|
-
join9(workDir, "audio-work"),
|
|
4987
|
-
audioOutputPath,
|
|
4988
|
-
duration,
|
|
4989
|
-
abortSignal,
|
|
4990
|
-
{
|
|
4991
|
-
ffmpegProcessTimeout,
|
|
4992
|
-
audioGain,
|
|
4993
|
-
masterFxChain,
|
|
4994
|
-
urlByAbsentLocalPath,
|
|
4995
|
-
keepEncoderInputAt
|
|
4996
|
-
},
|
|
4997
|
-
compiledDir
|
|
4998
|
-
);
|
|
4999
|
-
} catch (err) {
|
|
5000
|
-
assertNotAborted();
|
|
5001
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
5002
|
-
return {
|
|
5003
|
-
audioOutputPath,
|
|
5004
|
-
hasAudio: false,
|
|
5005
|
-
audioProcessMs: Date.now() - stage3Start,
|
|
5006
|
-
audioError: detail,
|
|
5007
|
-
// Synthesised rather than left undefined. The warning policy reads
|
|
5008
|
-
// owner, retryability, reason and stage off this list, so reporting the
|
|
5009
|
-
// FATAL failure — the one that took the whole mix down — with an empty
|
|
5010
|
-
// one gave it strictly less classification than a single dropped track
|
|
5011
|
-
// gets, which is the opposite of what this stage exists to do.
|
|
5012
|
-
// "internal" is the honest bucket: the stages enumerate ffmpeg steps
|
|
5013
|
-
// and this is the FX render, which is none of them.
|
|
5014
|
-
audioFailures: [
|
|
5015
|
-
{
|
|
5016
|
-
stage: "internal",
|
|
5017
|
-
reason: "internal",
|
|
5018
|
-
owner: "system",
|
|
5019
|
-
retryable: false,
|
|
5020
|
-
detail: detail.slice(0, 2e3)
|
|
5021
|
-
}
|
|
5022
|
-
]
|
|
5023
|
-
};
|
|
5024
|
-
}
|
|
5025
|
-
assertNotAborted();
|
|
5026
|
-
const interrupted = audioResult.failures?.find(
|
|
5027
|
-
(failure) => failure.reason === "external_interruption"
|
|
5028
|
-
);
|
|
5029
|
-
if (interrupted) {
|
|
5030
|
-
throw encoderFailureError("Audio processing failed", {
|
|
5031
|
-
error: interrupted.detail,
|
|
5032
|
-
failureReason: "external_interruption",
|
|
5033
|
-
audioFailures: audioFailureDiagnostics(triggerFirst(audioResult.failures, interrupted))
|
|
5034
|
-
});
|
|
5035
|
-
}
|
|
5036
|
-
hasAudio = audioResult.success;
|
|
5037
|
-
audioFailures = audioResult.failures;
|
|
5038
|
-
if (!hasAudio) {
|
|
5039
|
-
audioError = audioResult.error ?? "audio mix failed for an unknown reason";
|
|
5040
|
-
for (const failure of audioResult.failures ?? []) {
|
|
5041
|
-
log?.warn("Audio track failed", {
|
|
5042
|
-
elementId: failure.elementId,
|
|
5043
|
-
stage: failure.stage,
|
|
5044
|
-
reason: failure.reason,
|
|
5045
|
-
owner: failure.owner,
|
|
5046
|
-
retryable: failure.retryable,
|
|
5047
|
-
detail: failure.detail
|
|
5048
|
-
});
|
|
5049
|
-
}
|
|
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);
|
|
5050
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)}`);
|
|
5051
5011
|
}
|
|
5052
|
-
const
|
|
5012
|
+
const summary = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
|
|
5053
5013
|
return {
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
audioError,
|
|
5058
|
-
audioFailures
|
|
5014
|
+
peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
|
|
5015
|
+
firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
|
|
5016
|
+
laterBlocks: checkedDbfs(laterBlocks)
|
|
5059
5017
|
};
|
|
5060
5018
|
}
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
}
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
const
|
|
5087
|
-
const
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
return frameFileExtension(captureOptions.format) === "jpg" ? Math.ceil(
|
|
5091
|
-
rawBytes / ((captureOptions.quality ?? 80) > 80 ? HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR : JPEG_RAW_RGBA_DIVISOR)
|
|
5092
|
-
) : rawBytes;
|
|
5093
|
-
}
|
|
5094
|
-
function freeDiskBytesAt(path) {
|
|
5095
|
-
try {
|
|
5096
|
-
const stat = statfsSync2(path);
|
|
5097
|
-
return stat.bavail * stat.bsize;
|
|
5098
|
-
} catch {
|
|
5099
|
-
return null;
|
|
5100
|
-
}
|
|
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();
|
|
5101
5048
|
}
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
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)}`;
|
|
5109
5057
|
}
|
|
5110
|
-
function
|
|
5111
|
-
const
|
|
5112
|
-
|
|
5113
|
-
|
|
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
|
+
};
|
|
5114
5067
|
}
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
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.";
|
|
5122
|
-
default:
|
|
5123
|
-
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
|
+
};
|
|
5124
5074
|
}
|
|
5075
|
+
return {
|
|
5076
|
+
operation: "trim",
|
|
5077
|
+
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
5078
|
+
cleanupPaths: []
|
|
5079
|
+
};
|
|
5125
5080
|
}
|
|
5126
|
-
function
|
|
5127
|
-
|
|
5128
|
-
return new Error(
|
|
5129
|
-
`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}`
|
|
5130
|
-
);
|
|
5081
|
+
function formatSeconds(sec) {
|
|
5082
|
+
return sec.toFixed(6);
|
|
5131
5083
|
}
|
|
5132
|
-
function
|
|
5133
|
-
const
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
captureOptions,
|
|
5137
|
-
freeDiskBytes
|
|
5138
|
-
);
|
|
5139
|
-
if (headroom.available) return;
|
|
5140
|
-
throw diskCaptureShortfallError(
|
|
5141
|
-
headroom.estimatedBytes,
|
|
5142
|
-
headroom.freeBytes,
|
|
5143
|
-
framesDir,
|
|
5144
|
-
"estimate",
|
|
5145
|
-
remedy
|
|
5146
|
-
);
|
|
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);
|
|
5147
5088
|
}
|
|
5148
|
-
function
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
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
|
+
);
|
|
5161
5111
|
}
|
|
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
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
|
|
5205
|
-
if (frameRange !== void 0) {
|
|
5206
|
-
if (!Number.isFinite(frameRange.startFrame) || !Number.isFinite(frameRange.endFrame) || frameRange.startFrame < 0 || frameRange.endFrame <= frameRange.startFrame) {
|
|
5207
|
-
throw new Error(
|
|
5208
|
-
`[captureStage] invalid frameRange: ${JSON.stringify(frameRange)}. Expected non-negative startFrame strictly less than endFrame.`
|
|
5209
|
-
);
|
|
5210
|
-
}
|
|
5211
|
-
const rangeFrames = frameRange.endFrame - frameRange.startFrame;
|
|
5212
|
-
if (rangeFrames !== totalFrames) {
|
|
5213
|
-
throw new Error(
|
|
5214
|
-
`[captureStage] frameRange size (${rangeFrames}) must equal totalFrames (${totalFrames}). Received frameRange=${JSON.stringify(frameRange)}.`
|
|
5215
|
-
);
|
|
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
|
+
}
|
|
5216
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 });
|
|
5217
5166
|
}
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
});
|
|
5227
|
-
if (workerCount > 1) {
|
|
5228
|
-
const attempts = await executeDiskCaptureWithAdaptiveRetry({
|
|
5229
|
-
serverUrl: fileServer.url,
|
|
5230
|
-
workDir,
|
|
5231
|
-
framesDir,
|
|
5232
|
-
totalFrames,
|
|
5233
|
-
initialWorkerCount: workerCount,
|
|
5234
|
-
allowRetry: shouldAllowAdaptiveCaptureRetry(workerCount, job.config.workers !== void 0),
|
|
5235
|
-
frameExt: frameFileExtension(captureOptions.format),
|
|
5236
|
-
captureOptions,
|
|
5237
|
-
createBeforeCaptureHook: createRenderVideoFrameInjector,
|
|
5238
|
-
abortSignal,
|
|
5239
|
-
frameRangeStart: frameRange?.startFrame,
|
|
5240
|
-
dedupPerfs,
|
|
5241
|
-
onProgress: (progress) => {
|
|
5242
|
-
if (progress.latestWorkerPhase) {
|
|
5243
|
-
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
5244
|
-
return;
|
|
5245
|
-
}
|
|
5246
|
-
if (progress.landedFrame) input.onFrameLanded?.(progress.landedFrame);
|
|
5247
|
-
job.framesRendered = progress.capturedFrames;
|
|
5248
|
-
checkDiskProjection(progress.capturedFrames);
|
|
5249
|
-
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
5250
|
-
reportFrameProgress(
|
|
5251
|
-
job,
|
|
5252
|
-
`Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${progress.activeWorkers} workers)`,
|
|
5253
|
-
Math.round(25 + frameProgress * 45),
|
|
5254
|
-
onProgress,
|
|
5255
|
-
progress.capturedFrames,
|
|
5256
|
-
progress.totalFrames
|
|
5257
|
-
);
|
|
5258
|
-
},
|
|
5259
|
-
cfg: captureCfg,
|
|
5260
|
-
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
|
|
5261
5175
|
});
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
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
|
+
};
|
|
5272
5186
|
}
|
|
5273
|
-
|
|
5274
|
-
const sequential = await runSequentialDiskCapture(
|
|
5275
|
-
input,
|
|
5276
|
-
captureOptions,
|
|
5277
|
-
captureCfg,
|
|
5278
|
-
checkDiskProjection
|
|
5279
|
-
);
|
|
5280
|
-
probeSession = null;
|
|
5281
|
-
lastBrowserConsole = sequential.lastBrowserConsole;
|
|
5282
|
-
captureBeyondViewport = sequential.captureBeyondViewport;
|
|
5187
|
+
loweredDb = correction.loweredDb ?? 0;
|
|
5283
5188
|
}
|
|
5284
|
-
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
|
+
};
|
|
5285
5197
|
}
|
|
5286
|
-
async function
|
|
5287
|
-
const
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
let
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
videoInjector,
|
|
5312
|
-
captureCfg
|
|
5313
|
-
);
|
|
5314
|
-
captureBeyondViewport = session.options.captureBeyondViewport;
|
|
5315
|
-
try {
|
|
5316
|
-
if (probeSession) {
|
|
5317
|
-
prepareCaptureSessionForReuse3(session, framesDir, videoInjector);
|
|
5318
|
-
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" };
|
|
5319
5223
|
}
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
input,
|
|
5325
|
-
rangeStart,
|
|
5326
|
-
rangeEnd,
|
|
5327
|
-
resumeFrom,
|
|
5328
|
-
checkDiskProjection
|
|
5329
|
-
});
|
|
5330
|
-
break;
|
|
5331
|
-
} catch (error) {
|
|
5332
|
-
lastBrowserConsole = session.browserConsoleBuffer;
|
|
5333
|
-
const resume = planTransientResume(error, {
|
|
5334
|
-
abortSignal,
|
|
5335
|
-
rangeFrames,
|
|
5336
|
-
framesDir,
|
|
5337
|
-
frameExt,
|
|
5338
|
-
retriesUsed: transientRetriesUsed
|
|
5339
|
-
});
|
|
5340
|
-
if (resume !== null) {
|
|
5341
|
-
transientRetriesUsed++;
|
|
5342
|
-
resumeFrom = resume;
|
|
5343
|
-
log.warn(
|
|
5344
|
-
"[Render] Transient browser failure during sequential capture; retrying once with a fresh session.",
|
|
5345
|
-
{
|
|
5346
|
-
resumeFrom,
|
|
5347
|
-
rangeFrames,
|
|
5348
|
-
transientRetriesUsed,
|
|
5349
|
-
error: error instanceof Error ? error.message : String(error)
|
|
5350
|
-
}
|
|
5351
|
-
);
|
|
5352
|
-
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)) };
|
|
5353
5228
|
}
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
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;
|
|
5358
5249
|
}
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
}
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
}
|
|
5372
|
-
|
|
5373
|
-
if (!session.isInitialized) {
|
|
5374
|
-
await initializeSession5(session);
|
|
5375
|
-
return;
|
|
5376
|
-
}
|
|
5377
|
-
if (process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true") {
|
|
5378
|
-
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 });
|
|
5379
5264
|
}
|
|
5380
5265
|
}
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
`Capturing frame ${fileIndex + 1}/${rangeFrames}`,
|
|
5390
|
-
Math.round(25 + (fileIndex + 1) / rangeFrames * 45),
|
|
5391
|
-
onProgress,
|
|
5392
|
-
fileIndex + 1,
|
|
5393
|
-
rangeFrames
|
|
5394
|
-
);
|
|
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
|
|
5395
5274
|
};
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
const absoluteIdx = rangeStart + i;
|
|
5418
|
-
const time = absoluteIdx * job.config.fps.den / job.config.fps.num;
|
|
5419
|
-
await captureFrame(session, i, time);
|
|
5420
|
-
reportFrame(i);
|
|
5421
|
-
}
|
|
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 ?? "") };
|
|
5422
5296
|
}
|
|
5423
|
-
await
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
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
|
|
5433
5312
|
);
|
|
5434
|
-
|
|
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 ?? "") };
|
|
5435
5320
|
}
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
5445
|
-
} from "@hyperframes/engine";
|
|
5446
|
-
import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
|
|
5447
|
-
|
|
5448
|
-
// src/services/hdrImageTransferCache.ts
|
|
5449
|
-
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
5450
|
-
var DEFAULT_MAX_BYTES = 200 * 1024 * 1024;
|
|
5451
|
-
|
|
5452
|
-
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
5453
|
-
import {
|
|
5454
|
-
TRANSITIONS,
|
|
5455
|
-
crossfade
|
|
5456
|
-
} from "@hyperframes/engine";
|
|
5457
|
-
|
|
5458
|
-
// src/services/render/stages/captureHdrHybridLoop.ts
|
|
5459
|
-
import {
|
|
5460
|
-
TRANSITIONS as TRANSITIONS2,
|
|
5461
|
-
closeCaptureSession as closeCaptureSession6,
|
|
5462
|
-
createCaptureSession as createCaptureSession6,
|
|
5463
|
-
createFrameReorderBuffer as createFrameReorderBuffer2,
|
|
5464
|
-
crossfade as crossfade2,
|
|
5465
|
-
initTransparentBackground,
|
|
5466
|
-
initializeSession as initializeSession6
|
|
5467
|
-
} from "@hyperframes/engine";
|
|
5468
|
-
|
|
5469
|
-
// src/services/render/stages/encodeStage.ts
|
|
5470
|
-
import {
|
|
5471
|
-
copyFileSync as copyFileSync3,
|
|
5472
|
-
existsSync as existsSync9,
|
|
5473
|
-
mkdirSync as mkdirSync5,
|
|
5474
|
-
readdirSync as readdirSync2,
|
|
5475
|
-
readFileSync as readFileSync5,
|
|
5476
|
-
rmSync as rmSync4,
|
|
5477
|
-
statSync as statSync3,
|
|
5478
|
-
writeFileSync as writeFileSync2
|
|
5479
|
-
} from "node:fs";
|
|
5480
|
-
import { dirname as dirname5, join as join12 } from "node:path";
|
|
5481
|
-
import {
|
|
5482
|
-
encodeFramesChunkedConcat,
|
|
5483
|
-
encodeFramesFromDir,
|
|
5484
|
-
formatFfmpegError,
|
|
5485
|
-
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
|
|
5486
|
-
resolveConfig,
|
|
5487
|
-
runFfmpeg as runFfmpeg3,
|
|
5488
|
-
spawnStreamingEncoder as spawnStreamingEncoder4
|
|
5489
|
-
} from "@hyperframes/engine";
|
|
5490
|
-
import {
|
|
5491
|
-
clearGifFramesBeforeNext,
|
|
5492
|
-
gifClearsAfterLeavingFrameInPlace
|
|
5493
|
-
} from "@hyperframes/core";
|
|
5494
|
-
|
|
5495
|
-
// src/services/render/stages/gifEncodeArgs.ts
|
|
5496
|
-
import { join as join11 } from "node:path";
|
|
5497
|
-
function fpsToFfmpegArg2(fps) {
|
|
5498
|
-
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 };
|
|
5499
5329
|
}
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
|
|
5509
|
-
"-i",
|
|
5510
|
-
join11(input.framesDir, input.framePattern)
|
|
5511
|
-
];
|
|
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
|
+
};
|
|
5512
5338
|
}
|
|
5513
|
-
function
|
|
5514
|
-
|
|
5515
|
-
const transparency = input.preserveAlpha ? ":reserve_transparent=1" : "";
|
|
5516
|
-
return [
|
|
5517
|
-
"-y",
|
|
5518
|
-
...framesInput(input, fpsArg),
|
|
5519
|
-
"-vf",
|
|
5520
|
-
`fps=${fpsArg},palettegen=stats_mode=diff${transparency}`,
|
|
5521
|
-
input.palettePath
|
|
5522
|
-
];
|
|
5339
|
+
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
5340
|
+
return toStepResult(await runFfmpeg4(args, { signal, timeout: timeoutMs }), timeoutMs);
|
|
5523
5341
|
}
|
|
5524
|
-
function
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
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
|
+
}
|
|
5539
5393
|
}
|
|
5540
5394
|
|
|
5541
|
-
// src/services/render/stages/
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
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 });
|
|
5547
5429
|
}
|
|
5548
|
-
return resolved;
|
|
5549
5430
|
}
|
|
5550
|
-
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
|
+
}
|
|
5551
5511
|
return {
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
fileSize: 0,
|
|
5557
|
-
error,
|
|
5558
|
-
failureReason
|
|
5512
|
+
audioOutputPath,
|
|
5513
|
+
hasAudio,
|
|
5514
|
+
audioError,
|
|
5515
|
+
audioFailures
|
|
5559
5516
|
};
|
|
5560
5517
|
}
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
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;
|
|
5572
5546
|
}
|
|
5573
|
-
writeFileSync2(outputPath, gif);
|
|
5574
|
-
return null;
|
|
5575
5547
|
}
|
|
5576
|
-
|
|
5577
|
-
const
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
if (frameCount === 0) {
|
|
5581
|
-
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.`;
|
|
5582
5552
|
}
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
try {
|
|
5593
|
-
const run = { signal: input.signal, timeout: input.timeout };
|
|
5594
|
-
for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
|
|
5595
|
-
const result = await runFfmpeg3(args, run);
|
|
5596
|
-
if (!result.success) {
|
|
5597
|
-
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
5598
|
-
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
5599
|
-
}
|
|
5600
|
-
}
|
|
5601
|
-
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync5(outputPath)) !== false) {
|
|
5602
|
-
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
5603
|
-
if (failure) return failure;
|
|
5604
|
-
}
|
|
5605
|
-
const fileSize = existsSync9(outputPath) ? statSync3(outputPath).size : 0;
|
|
5606
|
-
return {
|
|
5607
|
-
success: true,
|
|
5608
|
-
outputPath,
|
|
5609
|
-
durationMs: Date.now() - startTime,
|
|
5610
|
-
framesEncoded: frameCount,
|
|
5611
|
-
fileSize
|
|
5612
|
-
};
|
|
5613
|
-
} finally {
|
|
5614
|
-
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.";
|
|
5615
5562
|
}
|
|
5616
5563
|
}
|
|
5617
|
-
function
|
|
5618
|
-
const
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
});
|
|
5623
|
-
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
|
+
);
|
|
5624
5569
|
}
|
|
5625
|
-
function
|
|
5626
|
-
|
|
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;
|
|
5627
5585
|
}
|
|
5628
|
-
function
|
|
5629
|
-
const {
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
5644
|
-
// renders leave both undefined → preserves the existing open-GOP output.
|
|
5645
|
-
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
5646
|
-
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
|
+
}
|
|
5647
5601
|
};
|
|
5648
5602
|
}
|
|
5649
|
-
async function
|
|
5603
|
+
async function runCaptureStage(input) {
|
|
5650
5604
|
const {
|
|
5605
|
+
fileServer,
|
|
5606
|
+
workDir,
|
|
5607
|
+
framesDir,
|
|
5651
5608
|
job,
|
|
5609
|
+
totalFrames,
|
|
5610
|
+
cfg,
|
|
5611
|
+
plan: plan2,
|
|
5652
5612
|
log,
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
needsAlpha,
|
|
5657
|
-
hasAudio,
|
|
5658
|
-
audioOutputPath,
|
|
5659
|
-
isPngSequence,
|
|
5660
|
-
isGif,
|
|
5661
|
-
enableChunkedEncode,
|
|
5662
|
-
chunkedEncodeSize,
|
|
5613
|
+
captureAttempts,
|
|
5614
|
+
buildCaptureOptions,
|
|
5615
|
+
createRenderVideoFrameInjector,
|
|
5663
5616
|
abortSignal,
|
|
5664
|
-
|
|
5665
|
-
|
|
5617
|
+
onProgress,
|
|
5618
|
+
frameRange,
|
|
5619
|
+
dedupPerfs
|
|
5666
5620
|
} = input;
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
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) {
|
|
5673
5629
|
throw new Error(
|
|
5674
|
-
`[
|
|
5630
|
+
`[captureStage] invalid frameRange: ${JSON.stringify(frameRange)}. Expected non-negative startFrame strictly less than endFrame.`
|
|
5675
5631
|
);
|
|
5676
5632
|
}
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
if (hasAudio && audioOutputPath && existsSync9(audioOutputPath)) {
|
|
5682
|
-
copyFileSync3(audioOutputPath, join12(outputPath, MIXED_AUDIO_FILENAME2));
|
|
5683
|
-
log.info(
|
|
5684
|
-
`[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)}.`
|
|
5685
5637
|
);
|
|
5686
5638
|
}
|
|
5687
|
-
encoded();
|
|
5688
|
-
return { encodeMs: Date.now() - stage5Start };
|
|
5689
5639
|
}
|
|
5690
|
-
const
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
const
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
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
|
|
5706
5682
|
});
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5683
|
+
captureAttempts.push(...attempts);
|
|
5684
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
5685
|
+
if (lastAttempt) {
|
|
5686
|
+
workerCount = lastAttempt.workers;
|
|
5710
5687
|
}
|
|
5711
|
-
|
|
5712
|
-
|
|
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;
|
|
5713
5704
|
}
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
const
|
|
5718
|
-
|
|
5719
|
-
const framePattern = `frame_%06d.${frameExt}`;
|
|
5720
|
-
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
5721
|
-
const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
|
|
5722
|
-
framesDir,
|
|
5723
|
-
framePattern,
|
|
5724
|
-
videoOnlyPath,
|
|
5725
|
-
encoderOpts,
|
|
5726
|
-
chunkedEncodeSize,
|
|
5727
|
-
abortSignal,
|
|
5728
|
-
videoEngineCfg,
|
|
5729
|
-
onFramesEncoded
|
|
5730
|
-
) : await encodeFramesFromDir(
|
|
5705
|
+
return { workerCount, probeSession, lastBrowserConsole, captureBeyondViewport };
|
|
5706
|
+
}
|
|
5707
|
+
async function runSequentialDiskCapture(input, captureOptions, captureCfg, checkDiskProjection) {
|
|
5708
|
+
const {
|
|
5709
|
+
fileServer,
|
|
5731
5710
|
framesDir,
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5711
|
+
totalFrames,
|
|
5712
|
+
log,
|
|
5713
|
+
createRenderVideoFrameInjector,
|
|
5735
5714
|
abortSignal,
|
|
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
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
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);
|
|
5793
5779
|
}
|
|
5794
|
-
};
|
|
5795
|
-
const result = await runFfmpeg4(
|
|
5796
|
-
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
|
|
5797
|
-
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
5798
|
-
);
|
|
5799
|
-
if (!result.success) {
|
|
5800
|
-
throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
|
|
5801
5780
|
}
|
|
5802
|
-
|
|
5803
|
-
return {
|
|
5804
|
-
peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
|
|
5805
|
-
firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
|
|
5806
|
-
laterBlocks: checkedDbfs(laterBlocks)
|
|
5807
|
-
};
|
|
5781
|
+
return { lastBrowserConsole, captureBeyondViewport };
|
|
5808
5782
|
}
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
|
|
5812
|
-
const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
|
|
5813
|
-
const block = 4800;
|
|
5814
|
-
const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
|
|
5815
|
-
if (count === 1) return wholeFile();
|
|
5816
|
-
const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
|
|
5817
|
-
const peers = new AbortController();
|
|
5818
|
-
const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
|
|
5819
|
-
const scans = await Promise.all(
|
|
5820
|
-
Array.from({ length: count }, (_, k) => {
|
|
5821
|
-
const start = k === 0 ? 0 : cut(k) - block;
|
|
5822
|
-
const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
|
|
5823
|
-
return scanTruePeak(
|
|
5824
|
-
audioPath,
|
|
5825
|
-
`atrim=start_sample=${start}${end},`,
|
|
5826
|
-
timeoutMs,
|
|
5827
|
-
segmentSignal
|
|
5828
|
-
);
|
|
5829
|
-
})
|
|
5830
|
-
).catch((error) => {
|
|
5831
|
-
peers.abort();
|
|
5832
|
-
throw error;
|
|
5833
|
-
});
|
|
5834
|
-
const last = scans[count - 1];
|
|
5835
|
-
const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
|
|
5836
|
-
if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
|
|
5837
|
-
return wholeFile();
|
|
5783
|
+
function resolveCaptureRange(frameRange, totalFrames) {
|
|
5784
|
+
return { rangeStart: frameRange?.startFrame ?? 0, rangeEnd: frameRange?.endFrame ?? totalFrames };
|
|
5838
5785
|
}
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
|
|
5846
|
-
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;
|
|
5847
5792
|
}
|
|
5848
|
-
function
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
return {
|
|
5853
|
-
operation: "copy",
|
|
5854
|
-
steps: [{ kind: "copy", args: ["-i", audioPath, "-c:a", "copy", "-y", outputPath] }],
|
|
5855
|
-
cleanupPaths: []
|
|
5856
|
-
};
|
|
5793
|
+
async function ensureSessionReady(session) {
|
|
5794
|
+
if (!session.isInitialized) {
|
|
5795
|
+
await initializeSession5(session);
|
|
5796
|
+
return;
|
|
5857
5797
|
}
|
|
5858
|
-
if (
|
|
5859
|
-
|
|
5860
|
-
operation: "pad",
|
|
5861
|
-
steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
|
|
5862
|
-
cleanupPaths: []
|
|
5863
|
-
};
|
|
5798
|
+
if (process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true") {
|
|
5799
|
+
await completeDeferredDrawElementInit3(session);
|
|
5864
5800
|
}
|
|
5865
|
-
return {
|
|
5866
|
-
operation: "trim",
|
|
5867
|
-
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
5868
|
-
cleanupPaths: []
|
|
5869
|
-
};
|
|
5870
|
-
}
|
|
5871
|
-
function formatSeconds(sec) {
|
|
5872
|
-
return sec.toFixed(6);
|
|
5873
|
-
}
|
|
5874
|
-
function sanitizeProbeFailure(reason, paths) {
|
|
5875
|
-
const message = reason instanceof Error ? reason.message : String(reason);
|
|
5876
|
-
const keepFfmpegsLastLineUncapped = Number.POSITIVE_INFINITY;
|
|
5877
|
-
return redactTelemetryString3(redactKnownPaths(message, paths), keepFfmpegsLastLineUncapped);
|
|
5878
5801
|
}
|
|
5879
|
-
async function
|
|
5880
|
-
const
|
|
5881
|
-
const
|
|
5882
|
-
const
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
return failResult(
|
|
5893
|
-
input.outputPath,
|
|
5894
|
-
0,
|
|
5895
|
-
audioResult.status === "fulfilled" ? audioResult.value.durationSeconds : 0,
|
|
5896
|
-
`audioPadTrim: failed to probe video: ${sanitizeProbeFailure(
|
|
5897
|
-
videoResult.reason,
|
|
5898
|
-
probePaths
|
|
5899
|
-
)}`
|
|
5900
|
-
);
|
|
5901
|
-
}
|
|
5902
|
-
if (audioResult.status === "rejected") {
|
|
5903
|
-
return failResult(
|
|
5904
|
-
input.outputPath,
|
|
5905
|
-
0,
|
|
5906
|
-
0,
|
|
5907
|
-
`audioPadTrim: failed to probe audio: ${sanitizeProbeFailure(
|
|
5908
|
-
audioResult.reason,
|
|
5909
|
-
probePaths
|
|
5910
|
-
)}`
|
|
5911
|
-
);
|
|
5912
|
-
}
|
|
5913
|
-
const videoInfo = videoResult.value;
|
|
5914
|
-
const audioInfo = audioResult.value;
|
|
5915
|
-
if (!Number.isFinite(videoInfo.frameCount) || videoInfo.frameCount <= 0 || !Number.isFinite(videoInfo.fpsNum) || videoInfo.fpsNum <= 0 || !Number.isFinite(videoInfo.fpsDen) || videoInfo.fpsDen <= 0) {
|
|
5916
|
-
return failResult(
|
|
5917
|
-
input.outputPath,
|
|
5918
|
-
0,
|
|
5919
|
-
audioInfo.durationSeconds,
|
|
5920
|
-
`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
|
|
5921
5815
|
);
|
|
5922
|
-
}
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
for (const step of plan2.steps) {
|
|
5932
|
-
const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
|
|
5933
|
-
if (!ffmpegResult.success) {
|
|
5934
|
-
return {
|
|
5935
|
-
success: false,
|
|
5936
|
-
outputPath: input.outputPath,
|
|
5937
|
-
targetDurationSeconds,
|
|
5938
|
-
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
5939
|
-
operation: plan2.operation,
|
|
5940
|
-
error: ffmpegResult.error,
|
|
5941
|
-
failureReason: ffmpegResult.failureReason
|
|
5942
|
-
};
|
|
5943
|
-
}
|
|
5944
|
-
}
|
|
5945
|
-
} catch (err) {
|
|
5946
|
-
return {
|
|
5947
|
-
success: false,
|
|
5948
|
-
outputPath: input.outputPath,
|
|
5949
|
-
targetDurationSeconds,
|
|
5950
|
-
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
5951
|
-
operation: plan2.operation,
|
|
5952
|
-
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);
|
|
5953
5825
|
};
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
targetDurationSeconds,
|
|
5971
|
-
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
5972
|
-
operation: plan2.operation,
|
|
5973
|
-
error: correction.error,
|
|
5974
|
-
failureReason: correction.failureReason
|
|
5975
|
-
};
|
|
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);
|
|
5976
5842
|
}
|
|
5977
|
-
loweredDb = correction.loweredDb ?? 0;
|
|
5978
5843
|
}
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
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));
|
|
5987
5856
|
}
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
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}`;
|
|
5997
5920
|
}
|
|
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
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
return {
|
|
6046
|
-
success: false,
|
|
6047
|
-
error: `audioPadTrim: failed to validate AAC true peak: ${sanitizeProbeFailure(
|
|
6048
|
-
err,
|
|
6049
|
-
scratchDir ? [input.audioPath, scratchDir] : [input.audioPath]
|
|
6050
|
-
)}`
|
|
6051
|
-
};
|
|
6052
|
-
} finally {
|
|
6053
|
-
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})`);
|
|
6054
5968
|
}
|
|
5969
|
+
return resolved;
|
|
6055
5970
|
}
|
|
6056
|
-
function
|
|
5971
|
+
function gifEncodeFailure(startTime, outputPath, error, failureReason) {
|
|
6057
5972
|
return {
|
|
6058
5973
|
success: false,
|
|
6059
5974
|
outputPath,
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
error
|
|
5975
|
+
durationMs: Date.now() - startTime,
|
|
5976
|
+
framesEncoded: 0,
|
|
5977
|
+
fileSize: 0,
|
|
5978
|
+
error,
|
|
5979
|
+
failureReason
|
|
6064
5980
|
};
|
|
6065
5981
|
}
|
|
6066
|
-
async function
|
|
6067
|
-
const
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
"v:0",
|
|
6073
|
-
"-show_entries",
|
|
6074
|
-
"stream=nb_frames,r_frame_rate",
|
|
6075
|
-
"-of",
|
|
6076
|
-
"json",
|
|
6077
|
-
"--",
|
|
6078
|
-
videoPath
|
|
6079
|
-
],
|
|
6080
|
-
signal
|
|
6081
|
-
);
|
|
6082
|
-
let stream = fastInfo.streams?.[0];
|
|
6083
|
-
const fastCount = Number(stream?.nb_frames);
|
|
6084
|
-
if (stream && Number.isFinite(fastCount) && fastCount > 0) {
|
|
6085
|
-
return { frameCount: fastCount, ...parseFrameRate(stream.r_frame_rate ?? "") };
|
|
6086
|
-
}
|
|
6087
|
-
const slowInfo = await runFfprobeJson(
|
|
6088
|
-
[
|
|
6089
|
-
"-v",
|
|
6090
|
-
"error",
|
|
6091
|
-
"-select_streams",
|
|
6092
|
-
"v:0",
|
|
6093
|
-
"-count_packets",
|
|
6094
|
-
"-show_entries",
|
|
6095
|
-
"stream=nb_read_packets,r_frame_rate",
|
|
6096
|
-
"-of",
|
|
6097
|
-
"json",
|
|
6098
|
-
"--",
|
|
6099
|
-
videoPath
|
|
6100
|
-
],
|
|
6101
|
-
signal
|
|
6102
|
-
);
|
|
6103
|
-
stream = slowInfo.streams?.[0];
|
|
6104
|
-
if (!stream) throw new Error(`ffprobe found no video stream in ${videoPath}`);
|
|
6105
|
-
const slowCount = Number(stream.nb_read_packets);
|
|
6106
|
-
if (!Number.isFinite(slowCount) || slowCount <= 0) {
|
|
6107
|
-
throw new Error(`ffprobe returned no frame count: ${JSON.stringify(stream)}`);
|
|
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);
|
|
6108
5988
|
}
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
const fpsNum = Number(n);
|
|
6114
|
-
const fpsDen = d === void 0 ? 1 : Number(d);
|
|
6115
|
-
if (!Number.isFinite(fpsNum) || !Number.isFinite(fpsDen) || fpsNum <= 0 || fpsDen <= 0) {
|
|
6116
|
-
throw new Error(`Invalid r_frame_rate: ${JSON.stringify(rate)}`);
|
|
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);
|
|
6117
5993
|
}
|
|
6118
|
-
|
|
5994
|
+
writeFileSync2(outputPath, gif);
|
|
5995
|
+
return null;
|
|
6119
5996
|
}
|
|
6120
|
-
async function
|
|
6121
|
-
const
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
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
|
|
6127
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
|
+
}
|
|
6128
6037
|
}
|
|
6129
|
-
|
|
6130
|
-
|
|
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);
|
|
6131
6045
|
}
|
|
6132
|
-
function
|
|
6133
|
-
return
|
|
6134
|
-
const options = pcmSourcePath ? { signal, timeout: timeoutMs, filter } : { signal, timeout: timeoutMs, filter, segments: 1 };
|
|
6135
|
-
return toStepResult(
|
|
6136
|
-
await encodeAac(pcmSourcePath ?? inputPath, outputPath, durationSeconds, options),
|
|
6137
|
-
timeoutMs
|
|
6138
|
-
);
|
|
6139
|
-
};
|
|
6046
|
+
function resolveEngineConfig(input) {
|
|
6047
|
+
return input.engineConfig ?? input.job.config.producerConfig ?? resolveConfig();
|
|
6140
6048
|
}
|
|
6141
|
-
function
|
|
6142
|
-
|
|
6049
|
+
function videoEncoderOptions(input, engineCfg) {
|
|
6050
|
+
const { job, preset } = input;
|
|
6143
6051
|
return {
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
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
|
|
6147
6068
|
};
|
|
6148
6069
|
}
|
|
6149
|
-
async function
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
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
|
+
);
|
|
6170
6097
|
}
|
|
6171
|
-
|
|
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 };
|
|
6172
6110
|
}
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
const
|
|
6176
|
-
|
|
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 };
|
|
6177
6134
|
}
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
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);
|
|
6182
6163
|
}
|
|
6164
|
+
onFramesEncoded();
|
|
6165
|
+
return { encodeMs: Date.now() - stage5Start };
|
|
6183
6166
|
}
|
|
6184
6167
|
|
|
6168
|
+
// src/services/render/stages/assembleStage.ts
|
|
6169
|
+
import { applyFaststart, muxVideoWithAudio, packageHls } from "@hyperframes/engine";
|
|
6170
|
+
|
|
6185
6171
|
// src/services/renderOrchestrator.ts
|
|
6186
6172
|
function sampleDirectoryBytes(dir) {
|
|
6187
6173
|
let total = 0;
|
|
@@ -6196,7 +6182,7 @@ function sampleDirectoryBytes(dir) {
|
|
|
6196
6182
|
continue;
|
|
6197
6183
|
}
|
|
6198
6184
|
for (const name of entries) {
|
|
6199
|
-
const full =
|
|
6185
|
+
const full = join13(current, name);
|
|
6200
6186
|
try {
|
|
6201
6187
|
const st = statSync4(full);
|
|
6202
6188
|
if (st.isDirectory()) {
|
|
@@ -6281,7 +6267,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
6281
6267
|
const ranges = [];
|
|
6282
6268
|
let rangeStart = null;
|
|
6283
6269
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
6284
|
-
const framePath =
|
|
6270
|
+
const framePath = join13(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
6285
6271
|
const missing = !existsSync10(framePath) || statSync4(framePath).size <= 8;
|
|
6286
6272
|
if (missing && rangeStart === null) {
|
|
6287
6273
|
rangeStart = frameIndex;
|
|
@@ -6304,7 +6290,7 @@ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, ran
|
|
|
6304
6290
|
workerId,
|
|
6305
6291
|
startFrame: rangeStart + range.startFrame,
|
|
6306
6292
|
endFrame: rangeStart + range.endFrame,
|
|
6307
|
-
outputDir:
|
|
6293
|
+
outputDir: join13(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
|
|
6308
6294
|
outputFrameOffset: rangeStart
|
|
6309
6295
|
}));
|
|
6310
6296
|
batches.push(batch);
|
|
@@ -6330,7 +6316,7 @@ function isRecoverableParallelCaptureError(error) {
|
|
|
6330
6316
|
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
6331
6317
|
let captured = 0;
|
|
6332
6318
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
6333
|
-
const framePath =
|
|
6319
|
+
const framePath = join13(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
6334
6320
|
if (existsSync10(framePath)) captured++;
|
|
6335
6321
|
}
|
|
6336
6322
|
return captured;
|
|
@@ -6356,7 +6342,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
6356
6342
|
reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
|
|
6357
6343
|
});
|
|
6358
6344
|
pendingTransientRetry = false;
|
|
6359
|
-
const attemptWorkDir =
|
|
6345
|
+
const attemptWorkDir = join13(options.workDir, `capture-attempt-${attempt}`);
|
|
6360
6346
|
const batches = missingRanges ? buildMissingFrameRetryBatches(
|
|
6361
6347
|
missingRanges,
|
|
6362
6348
|
currentWorkers,
|
|
@@ -6535,7 +6521,7 @@ function explainStreamingEncodeGate(cfg, outputFormat, workerCount, durationSeco
|
|
|
6535
6521
|
var DEFAULT_SEGMENTED_MIN_SECONDS = Number.POSITIVE_INFINITY;
|
|
6536
6522
|
|
|
6537
6523
|
// src/services/distributed/planFrameCost.ts
|
|
6538
|
-
import { join as
|
|
6524
|
+
import { join as join16 } from "node:path";
|
|
6539
6525
|
import {
|
|
6540
6526
|
closeCaptureSession as closeCaptureSession9,
|
|
6541
6527
|
createCaptureSession as createCaptureSession9,
|
|
@@ -6545,7 +6531,7 @@ import {
|
|
|
6545
6531
|
|
|
6546
6532
|
// src/services/distributed/chunkFrames.ts
|
|
6547
6533
|
import { existsSync as existsSync12, readdirSync as readdirSync4 } from "node:fs";
|
|
6548
|
-
import { extname as extname2, join as
|
|
6534
|
+
import { extname as extname2, join as join15 } from "node:path";
|
|
6549
6535
|
import {
|
|
6550
6536
|
compositionRequiresWebGpu as compositionRequiresWebGpu3,
|
|
6551
6537
|
createFrameLookupTable as createFrameLookupTable2,
|
|
@@ -6555,7 +6541,7 @@ import {
|
|
|
6555
6541
|
|
|
6556
6542
|
// src/services/distributed/shared.ts
|
|
6557
6543
|
import { execFile as execFileCallback } from "node:child_process";
|
|
6558
|
-
import { dirname as dirname8, join as
|
|
6544
|
+
import { dirname as dirname8, join as join14 } from "node:path";
|
|
6559
6545
|
import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
|
|
6560
6546
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6561
6547
|
import { promisify as promisify2 } from "node:util";
|
|
@@ -6571,7 +6557,7 @@ function isPlanAudioArtifactPath(path) {
|
|
|
6571
6557
|
}
|
|
6572
6558
|
function resolvePlanAudioPath(planDir) {
|
|
6573
6559
|
for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
|
|
6574
|
-
const candidate =
|
|
6560
|
+
const candidate = join14(planDir, name);
|
|
6575
6561
|
if (existsSync11(candidate)) return candidate;
|
|
6576
6562
|
}
|
|
6577
6563
|
return null;
|
|
@@ -6827,7 +6813,7 @@ function readProducerVersion() {
|
|
|
6827
6813
|
const startDir = dirname8(fileURLToPath3(import.meta.url));
|
|
6828
6814
|
let current = startDir;
|
|
6829
6815
|
for (let i = 0; i < 10; i++) {
|
|
6830
|
-
const candidate =
|
|
6816
|
+
const candidate = join14(current, "package.json");
|
|
6831
6817
|
if (existsSync11(candidate)) {
|
|
6832
6818
|
try {
|
|
6833
6819
|
const pkg = JSON.parse(readFileSync7(candidate, "utf-8"));
|
|
@@ -6880,7 +6866,7 @@ function frameNumberFromFileName(name) {
|
|
|
6880
6866
|
function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
|
|
6881
6867
|
const result = [];
|
|
6882
6868
|
for (const v of videos) {
|
|
6883
|
-
const outputDir =
|
|
6869
|
+
const outputDir = join15(planDir, "video-frames", v.videoId);
|
|
6884
6870
|
if (!existsSync12(outputDir)) {
|
|
6885
6871
|
throw new Error(
|
|
6886
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.`
|
|
@@ -6899,7 +6885,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
6899
6885
|
if (!frameName) continue;
|
|
6900
6886
|
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
|
|
6901
6887
|
const frameIndex = frameNumber === null ? i : frameNumber - 1;
|
|
6902
|
-
framePaths.set(frameIndex,
|
|
6888
|
+
framePaths.set(frameIndex, join15(outputDir, frameName));
|
|
6903
6889
|
}
|
|
6904
6890
|
result.push({
|
|
6905
6891
|
videoId: v.videoId,
|
|
@@ -6941,7 +6927,7 @@ function buildChunkCaptureOptions(input) {
|
|
|
6941
6927
|
skipReadinessVideoIds: input.skipReadinessVideoIds,
|
|
6942
6928
|
videoMetadataHints: input.videoMetadataHints,
|
|
6943
6929
|
...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {},
|
|
6944
|
-
pageMotionBlur: "distributed-render",
|
|
6930
|
+
pageMotionBlur: encoder.pageMotionBlur ?? "distributed-render",
|
|
6945
6931
|
// A fixed warmup count keeps `beginFrameTimeTicks` host-independent.
|
|
6946
6932
|
lockWarmupTicks: true,
|
|
6947
6933
|
requiresWebGpu: compositionRequiresWebGpu3(input.html)
|
|
@@ -6989,14 +6975,14 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
6989
6975
|
const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
|
|
6990
6976
|
return Promise.all(
|
|
6991
6977
|
deferred.map(async (video) => {
|
|
6992
|
-
const outputDir =
|
|
6978
|
+
const outputDir = join15(input.outputRoot, video.videoId);
|
|
6993
6979
|
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
6994
6980
|
if (indexes.length === 0) {
|
|
6995
6981
|
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
6996
6982
|
}
|
|
6997
6983
|
const range = video.deferredRange;
|
|
6998
6984
|
const remoteUrl = remoteSources[range.sourcePath];
|
|
6999
|
-
const sourcePath = remoteUrl ??
|
|
6985
|
+
const sourcePath = remoteUrl ?? join15(input.planDir, ...range.sourcePath.split("/"));
|
|
7000
6986
|
if (remoteUrl === void 0 && !existsSync12(sourcePath)) {
|
|
7001
6987
|
throw new RenderChunkValidationError(
|
|
7002
6988
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -7045,12 +7031,12 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
7045
7031
|
planVideos,
|
|
7046
7032
|
frames: sampled,
|
|
7047
7033
|
fps: fpsRatio,
|
|
7048
|
-
outputRoot:
|
|
7034
|
+
outputRoot: join16(workDir, "frame-cost-video-frames"),
|
|
7049
7035
|
cfg: input.cfg
|
|
7050
7036
|
})
|
|
7051
7037
|
];
|
|
7052
7038
|
const lookup = extracted.length > 0 ? createFrameLookupTable3(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
|
|
7053
|
-
const compiledDir =
|
|
7039
|
+
const compiledDir = join16(planDir, "compiled");
|
|
7054
7040
|
const fileServer = await createFileServer({
|
|
7055
7041
|
projectDir: compiledDir,
|
|
7056
7042
|
compiledDir,
|
|
@@ -7062,7 +7048,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
7062
7048
|
try {
|
|
7063
7049
|
session = await createCaptureSession9(
|
|
7064
7050
|
fileServer.url,
|
|
7065
|
-
|
|
7051
|
+
join16(workDir, "frame-cost"),
|
|
7066
7052
|
buildChunkCaptureOptions({
|
|
7067
7053
|
dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
|
|
7068
7054
|
encoder: input,
|
|
@@ -7084,7 +7070,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
7084
7070
|
|
|
7085
7071
|
// src/services/render/stages/freezePlan.ts
|
|
7086
7072
|
import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7087
|
-
import { join as
|
|
7073
|
+
import { join as join17, relative as relative3, resolve as resolve7 } from "node:path";
|
|
7088
7074
|
|
|
7089
7075
|
// src/services/distributed/planProtocol.ts
|
|
7090
7076
|
var PLAN_SCHEMA_VERSION = 1;
|
|
@@ -7302,7 +7288,7 @@ function listPlanFiles(planDir) {
|
|
|
7302
7288
|
function walk(dir) {
|
|
7303
7289
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
7304
7290
|
for (const entry of entries) {
|
|
7305
|
-
const full =
|
|
7291
|
+
const full = join17(dir, entry.name);
|
|
7306
7292
|
if (entry.isDirectory()) {
|
|
7307
7293
|
walk(full);
|
|
7308
7294
|
} else if (entry.isFile()) {
|
|
@@ -7343,15 +7329,15 @@ var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
|
|
|
7343
7329
|
function writePlanRemoteFiles(planDir, files) {
|
|
7344
7330
|
if (files.length === 0) return;
|
|
7345
7331
|
const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
|
|
7346
|
-
writeFileSync4(
|
|
7332
|
+
writeFileSync4(join17(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
|
|
7347
7333
|
}
|
|
7348
7334
|
function readPlanRemoteFiles(planDir) {
|
|
7349
|
-
const path =
|
|
7335
|
+
const path = join17(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
|
|
7350
7336
|
return existsSync13(path) ? JSON.parse(readFileSync8(path, "utf-8")) : [];
|
|
7351
7337
|
}
|
|
7352
7338
|
function recomputePlanHashFromPlanDir(planDir, digests) {
|
|
7353
|
-
const planJsonPath =
|
|
7354
|
-
const encoderJsonPath =
|
|
7339
|
+
const planJsonPath = join17(planDir, "plan.json");
|
|
7340
|
+
const encoderJsonPath = join17(planDir, "meta", "encoder.json");
|
|
7355
7341
|
if (!existsSync13(planJsonPath)) {
|
|
7356
7342
|
throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
|
|
7357
7343
|
}
|
|
@@ -7388,18 +7374,18 @@ async function freezePlan(input) {
|
|
|
7388
7374
|
if (!existsSync13(planDir)) {
|
|
7389
7375
|
throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
|
|
7390
7376
|
}
|
|
7391
|
-
const metaDir =
|
|
7377
|
+
const metaDir = join17(planDir, "meta");
|
|
7392
7378
|
if (!existsSync13(metaDir)) mkdirSync7(metaDir, { recursive: true });
|
|
7393
7379
|
writeFileSync4(
|
|
7394
|
-
|
|
7380
|
+
join17(metaDir, "composition.json"),
|
|
7395
7381
|
`${JSON.stringify(composition, null, 2)}
|
|
7396
7382
|
`,
|
|
7397
7383
|
"utf-8"
|
|
7398
7384
|
);
|
|
7399
7385
|
const encoderForCanonical = stripUndefined(encoder);
|
|
7400
7386
|
const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
|
|
7401
|
-
writeFileSync4(
|
|
7402
|
-
writeFileSync4(
|
|
7387
|
+
writeFileSync4(join17(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
|
|
7388
|
+
writeFileSync4(join17(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
|
|
7403
7389
|
`, "utf-8");
|
|
7404
7390
|
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
7405
7391
|
const planHash = computePlanHash({
|
|
@@ -7423,7 +7409,7 @@ async function freezePlan(input) {
|
|
|
7423
7409
|
duration: durationSeconds,
|
|
7424
7410
|
hasAudio
|
|
7425
7411
|
};
|
|
7426
|
-
const planJsonPath =
|
|
7412
|
+
const planJsonPath = join17(planDir, "plan.json");
|
|
7427
7413
|
writeFileSync4(planJsonPath, `${JSON.stringify(planJson, null, 2)}
|
|
7428
7414
|
`, "utf-8");
|
|
7429
7415
|
return { planJsonPath, planHash };
|
|
@@ -7467,7 +7453,7 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
|
|
|
7467
7453
|
// src/services/distributed/chunkAudio.ts
|
|
7468
7454
|
import { closeSync as closeSync4, existsSync as existsSync14, ftruncateSync, mkdirSync as mkdirSync9, openSync as openSync4, readFileSync as readFileSync9 } from "node:fs";
|
|
7469
7455
|
import { writeFileSync as writeFileSync5, writeSync } from "node:fs";
|
|
7470
|
-
import { join as
|
|
7456
|
+
import { join as join19 } from "node:path";
|
|
7471
7457
|
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
|
|
7472
7458
|
|
|
7473
7459
|
// src/services/distributed/mp4Stitch.ts
|
|
@@ -7477,10 +7463,10 @@ import {
|
|
|
7477
7463
|
mkdirSync as mkdirSync8,
|
|
7478
7464
|
mkdtempSync as mkdtempSync3,
|
|
7479
7465
|
renameSync as renameSync3,
|
|
7480
|
-
rmSync as
|
|
7466
|
+
rmSync as rmSync8
|
|
7481
7467
|
} from "node:fs";
|
|
7482
7468
|
import { open } from "node:fs/promises";
|
|
7483
|
-
import { dirname as dirname9, join as
|
|
7469
|
+
import { dirname as dirname9, join as join18 } from "node:path";
|
|
7484
7470
|
import { finished, pipeline } from "node:stream/promises";
|
|
7485
7471
|
function boxAt(src, off, at, limit) {
|
|
7486
7472
|
let size = src.readUInt32BE(off);
|
|
@@ -7846,16 +7832,16 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
|
7846
7832
|
const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
|
|
7847
7833
|
mkdirSync8(dirname9(outputPath), { recursive: true });
|
|
7848
7834
|
const workDir = mkdtempSync3(`${outputPath}.stitch-`);
|
|
7849
|
-
const out = createWriteStream(
|
|
7835
|
+
const out = createWriteStream(join18(workDir, "out.mp4"));
|
|
7850
7836
|
try {
|
|
7851
7837
|
out.write(header);
|
|
7852
7838
|
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
7853
7839
|
out.end();
|
|
7854
7840
|
await finished(out);
|
|
7855
|
-
renameSync3(
|
|
7841
|
+
renameSync3(join18(workDir, "out.mp4"), outputPath);
|
|
7856
7842
|
} finally {
|
|
7857
7843
|
out.destroy();
|
|
7858
|
-
|
|
7844
|
+
rmSync8(workDir, { recursive: true, force: true });
|
|
7859
7845
|
}
|
|
7860
7846
|
}
|
|
7861
7847
|
|
|
@@ -7946,7 +7932,7 @@ async function writeChunkAudioSlices(input) {
|
|
|
7946
7932
|
});
|
|
7947
7933
|
const bytes = readFileSync9(input.audioPath);
|
|
7948
7934
|
const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
|
|
7949
|
-
mkdirSync9(
|
|
7935
|
+
mkdirSync9(join19(input.planDir, SLICE_DIR), { recursive: true });
|
|
7950
7936
|
input.chunks.forEach((_, index) => {
|
|
7951
7937
|
const cut = audioCutForChunk(grid, input.chunks, index, input.fps);
|
|
7952
7938
|
if (!cut) return;
|
|
@@ -7954,17 +7940,17 @@ async function writeChunkAudioSlices(input) {
|
|
|
7954
7940
|
const start = offsets[cut.firstPacket];
|
|
7955
7941
|
layout.partStarts[index] = start;
|
|
7956
7942
|
const end = offsets[last] + track.sizes[last];
|
|
7957
|
-
writeFileSync5(
|
|
7943
|
+
writeFileSync5(join19(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
|
|
7958
7944
|
});
|
|
7959
7945
|
const frame = [bytes.subarray(0, payloadStart), bytes.subarray(payloadEnd)];
|
|
7960
|
-
writeFileSync5(
|
|
7961
|
-
writeFileSync5(
|
|
7946
|
+
writeFileSync5(join19(input.planDir, SLICE_FRAME), Buffer.concat(frame));
|
|
7947
|
+
writeFileSync5(join19(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
|
|
7962
7948
|
}
|
|
7963
7949
|
function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
7964
|
-
const layoutPath =
|
|
7950
|
+
const layoutPath = join19(planDir, SLICE_LAYOUT);
|
|
7965
7951
|
if (!existsSync14(layoutPath)) return resolvePlanAudioPath(planDir);
|
|
7966
7952
|
const layout = JSON.parse(readFileSync9(layoutPath, "utf-8"));
|
|
7967
|
-
const frame = readFileSync9(
|
|
7953
|
+
const frame = readFileSync9(join19(planDir, SLICE_FRAME));
|
|
7968
7954
|
const fd = openSync4(scratchPath, "w");
|
|
7969
7955
|
try {
|
|
7970
7956
|
writeSync(fd, frame, 0, layout.payloadStart, 0);
|
|
@@ -7977,7 +7963,7 @@ function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
|
7977
7963
|
);
|
|
7978
7964
|
const start = layout.partStarts[chunkIndex];
|
|
7979
7965
|
if (start !== void 0) {
|
|
7980
|
-
const part = readFileSync9(
|
|
7966
|
+
const part = readFileSync9(join19(planDir, chunkAudioSlicePath(chunkIndex)));
|
|
7981
7967
|
writeSync(fd, part, 0, part.length, start);
|
|
7982
7968
|
}
|
|
7983
7969
|
ftruncateSync(fd, layout.sizeBytes);
|
|
@@ -8033,7 +8019,7 @@ async function muxChunkWithAudio(input) {
|
|
|
8033
8019
|
// src/services/distributed/planSize.ts
|
|
8034
8020
|
import { createHash as createHash5 } from "node:crypto";
|
|
8035
8021
|
import { lstatSync, readdirSync as readdirSync6 } from "node:fs";
|
|
8036
|
-
import { extname as extname3, join as
|
|
8022
|
+
import { extname as extname3, join as join20, relative as relative4 } from "node:path";
|
|
8037
8023
|
var MAX_TOP_COMPONENTS = 10;
|
|
8038
8024
|
var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
8039
8025
|
".aac",
|
|
@@ -8145,7 +8131,7 @@ function walkRegularFiles(dir, visit) {
|
|
|
8145
8131
|
return;
|
|
8146
8132
|
}
|
|
8147
8133
|
for (const entry of entries) {
|
|
8148
|
-
const full =
|
|
8134
|
+
const full = join20(dir, entry.name);
|
|
8149
8135
|
if (entry.isDirectory()) {
|
|
8150
8136
|
walkRegularFiles(full, visit);
|
|
8151
8137
|
} else if (entry.isFile()) {
|
|
@@ -8221,7 +8207,6 @@ function chunkSizingUsesFrameCost(config) {
|
|
|
8221
8207
|
}
|
|
8222
8208
|
var CHUNK_EXCEEDS_WORKER_TIMEOUT = "CHUNK_EXCEEDS_WORKER_TIMEOUT";
|
|
8223
8209
|
var ChunkExceedsWorkerTimeoutError = class extends Error {
|
|
8224
|
-
// fallow-ignore-next-line unused-class-member
|
|
8225
8210
|
code = CHUNK_EXCEEDS_WORKER_TIMEOUT;
|
|
8226
8211
|
constructor(predictedChunkSeconds, workerTimeoutSeconds) {
|
|
8227
8212
|
super(
|
|
@@ -8284,7 +8269,6 @@ var PlanTooLargeError = class extends Error {
|
|
|
8284
8269
|
};
|
|
8285
8270
|
var FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED = "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED";
|
|
8286
8271
|
var FormatNotSupportedInDistributedError = class extends Error {
|
|
8287
|
-
// fallow-ignore-next-line unused-class-member
|
|
8288
8272
|
code = FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED;
|
|
8289
8273
|
format;
|
|
8290
8274
|
reason;
|
|
@@ -8359,13 +8343,13 @@ function assertPositiveInteger(name, value) {
|
|
|
8359
8343
|
}
|
|
8360
8344
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
8361
8345
|
"plan.json",
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8346
|
+
join21("meta", "composition.json"),
|
|
8347
|
+
join21("meta", "encoder.json"),
|
|
8348
|
+
join21("meta", "chunks.json")
|
|
8365
8349
|
];
|
|
8366
8350
|
function removeFreezeOwnedPlanFiles(planDir) {
|
|
8367
8351
|
for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
|
|
8368
|
-
|
|
8352
|
+
rmSync9(join21(planDir, relativePath), { force: true });
|
|
8369
8353
|
}
|
|
8370
8354
|
}
|
|
8371
8355
|
function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
@@ -8424,6 +8408,7 @@ function buildLockedRenderConfig(input) {
|
|
|
8424
8408
|
chunkCount: input.chunkCount,
|
|
8425
8409
|
runtimeEnv: input.runtimeEnv,
|
|
8426
8410
|
variables: config.variables,
|
|
8411
|
+
pageMotionBlur: config.pageMotionBlur,
|
|
8427
8412
|
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {},
|
|
8428
8413
|
...input.separateAudio ? { separateAudio: true } : {}
|
|
8429
8414
|
};
|
|
@@ -8497,19 +8482,19 @@ function createVideoSourceShipper(input) {
|
|
|
8497
8482
|
const known = shipped.get(srcPath);
|
|
8498
8483
|
if (known) return known;
|
|
8499
8484
|
const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
|
|
8500
|
-
const current = stagedRel === null ? srcPath :
|
|
8485
|
+
const current = stagedRel === null ? srcPath : join21(input.planDir, "video-frames", stagedRel);
|
|
8501
8486
|
let result = null;
|
|
8502
8487
|
for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
|
|
8503
8488
|
const rel = pathInside(root, srcPath);
|
|
8504
|
-
if (rel !== null && (existsSync15(
|
|
8489
|
+
if (rel !== null && (existsSync15(join21(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
|
|
8505
8490
|
result = ["compiled", ...rel.split(sep2)].join("/");
|
|
8506
8491
|
break;
|
|
8507
8492
|
}
|
|
8508
8493
|
}
|
|
8509
8494
|
if (result === null) {
|
|
8510
8495
|
result = `video-sources/${shipped.size}${extname4(current)}`;
|
|
8511
|
-
const destination =
|
|
8512
|
-
mkdirSync10(
|
|
8496
|
+
const destination = join21(input.planDir, ...result.split("/"));
|
|
8497
|
+
mkdirSync10(join21(input.planDir, "video-sources"), { recursive: true });
|
|
8513
8498
|
try {
|
|
8514
8499
|
linkSync(current, destination);
|
|
8515
8500
|
} catch {
|
|
@@ -8521,7 +8506,7 @@ function createVideoSourceShipper(input) {
|
|
|
8521
8506
|
};
|
|
8522
8507
|
}
|
|
8523
8508
|
function plansSeparateAudio(config, options) {
|
|
8524
|
-
return options.deferAudio === true && config.format
|
|
8509
|
+
return options.deferAudio === true && config.format !== "png-sequence" && config.cfr !== true;
|
|
8525
8510
|
}
|
|
8526
8511
|
async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
8527
8512
|
rejectUnsupportedDistributedFormat(config);
|
|
@@ -8558,14 +8543,14 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
8558
8543
|
variables: config.variables
|
|
8559
8544
|
});
|
|
8560
8545
|
const entryFile = config.entryFile ?? "index.html";
|
|
8561
|
-
const htmlPath =
|
|
8546
|
+
const htmlPath = join21(projectDir, entryFile);
|
|
8562
8547
|
if (!existsSync15(htmlPath)) {
|
|
8563
8548
|
throw new Error(`[plan] entry file not found: ${htmlPath}`);
|
|
8564
8549
|
}
|
|
8565
|
-
const workDir =
|
|
8566
|
-
|
|
8550
|
+
const workDir = join21(planDir, ".plan-work");
|
|
8551
|
+
rmSync9(workDir, { recursive: true, force: true });
|
|
8567
8552
|
mkdirSync10(workDir, { recursive: true });
|
|
8568
|
-
const compiledDir =
|
|
8553
|
+
const compiledDir = join21(workDir, "compiled");
|
|
8569
8554
|
mkdirSync10(compiledDir, { recursive: true });
|
|
8570
8555
|
let stageStarted = Date.now();
|
|
8571
8556
|
cpSync2(projectDir, compiledDir, {
|
|
@@ -8579,10 +8564,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
8579
8564
|
}
|
|
8580
8565
|
});
|
|
8581
8566
|
logPlanStage(log, "copy", stageStarted);
|
|
8582
|
-
const finalCompiledDir =
|
|
8567
|
+
const finalCompiledDir = join21(planDir, "compiled");
|
|
8583
8568
|
const needsAlpha = outputNeedsAlpha(config.format);
|
|
8584
8569
|
const remoteFiles = Object.entries(options.remoteFiles ?? {});
|
|
8585
|
-
const remoteLocalPath = (rel) =>
|
|
8570
|
+
const remoteLocalPath = (rel) => join21(projectDir, ...rel.split("/"));
|
|
8586
8571
|
const urlByAbsentLocalPath = new Map(
|
|
8587
8572
|
remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
|
|
8588
8573
|
);
|
|
@@ -8681,22 +8666,21 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
8681
8666
|
forceScreenshot,
|
|
8682
8667
|
width,
|
|
8683
8668
|
height,
|
|
8684
|
-
probeResult,
|
|
8685
8669
|
totalFrames
|
|
8686
8670
|
};
|
|
8687
8671
|
}
|
|
8688
|
-
function mixPlanAudio(c, compiledDir
|
|
8672
|
+
function mixPlanAudio(c, compiledDir) {
|
|
8689
8673
|
return runAudioStage({
|
|
8690
8674
|
projectDir: c.projectDir,
|
|
8691
8675
|
workDir: c.workDir,
|
|
8692
8676
|
compiledDir,
|
|
8693
8677
|
urlByAbsentLocalPath: c.urlByAbsentLocalPath,
|
|
8694
|
-
|
|
8678
|
+
totalFrames: c.totalFrames,
|
|
8679
|
+
fps: c.job.config.fps,
|
|
8695
8680
|
ffmpegProcessTimeout: c.cfg.ffmpegProcessTimeout,
|
|
8696
8681
|
audioGain: c.cfg.audioGain,
|
|
8697
8682
|
audios: c.composition.audios,
|
|
8698
8683
|
masterFxChain: c.composition.masterFxChain,
|
|
8699
|
-
...pcmPath ? { keepEncoderInputAt: pcmPath } : {},
|
|
8700
8684
|
abortSignal: c.abortSignal,
|
|
8701
8685
|
assertNotAborted: c.assertNotAborted
|
|
8702
8686
|
});
|
|
@@ -8720,16 +8704,6 @@ async function extractPlanVideos(c, deferRangeExtraction) {
|
|
|
8720
8704
|
}
|
|
8721
8705
|
return extractResult;
|
|
8722
8706
|
}
|
|
8723
|
-
async function holdPlanAudioPeak(c, audioPath, pcmPath, seconds) {
|
|
8724
|
-
const peak = await constrainAacTruePeak({
|
|
8725
|
-
audioPath,
|
|
8726
|
-
...existsSync15(pcmPath) ? { sourcePath: pcmPath } : {},
|
|
8727
|
-
targetDurationSeconds: seconds,
|
|
8728
|
-
signal: c.abortSignal,
|
|
8729
|
-
timeoutMs: c.cfg.ffmpegProcessTimeout
|
|
8730
|
-
}).finally(() => rmSync8(pcmPath, { force: true }));
|
|
8731
|
-
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
8732
|
-
}
|
|
8733
8707
|
async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, options = {}) {
|
|
8734
8708
|
const planDir = executionPlanDir;
|
|
8735
8709
|
const c = await compilePlanComposition(projectDir, config, planDir, options);
|
|
@@ -8751,7 +8725,6 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8751
8725
|
forceScreenshot,
|
|
8752
8726
|
width,
|
|
8753
8727
|
height,
|
|
8754
|
-
probeResult,
|
|
8755
8728
|
totalFrames
|
|
8756
8729
|
} = c;
|
|
8757
8730
|
const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
|
|
@@ -8767,17 +8740,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8767
8740
|
let stageStarted = Date.now();
|
|
8768
8741
|
const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
|
|
8769
8742
|
logPlanStage(log, "extract", stageStarted);
|
|
8770
|
-
const stagedVideoFrames =
|
|
8771
|
-
const videoFramesDst =
|
|
8772
|
-
if (existsSync15(videoFramesDst))
|
|
8773
|
-
const videoSourcesDir =
|
|
8774
|
-
|
|
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 });
|
|
8775
8748
|
if (existsSync15(stagedVideoFrames)) {
|
|
8776
8749
|
renameSync4(stagedVideoFrames, videoFramesDst);
|
|
8777
8750
|
} else {
|
|
8778
8751
|
mkdirSync10(videoFramesDst, { recursive: true });
|
|
8779
8752
|
}
|
|
8780
|
-
if (existsSync15(finalCompiledDir))
|
|
8753
|
+
if (existsSync15(finalCompiledDir)) rmSync9(finalCompiledDir, { recursive: true, force: true });
|
|
8781
8754
|
renameSync4(compiledDir, finalCompiledDir);
|
|
8782
8755
|
const shipVideoSource = createVideoSourceShipper({
|
|
8783
8756
|
planDir,
|
|
@@ -8807,7 +8780,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8807
8780
|
};
|
|
8808
8781
|
})
|
|
8809
8782
|
});
|
|
8810
|
-
mkdirSync10(
|
|
8783
|
+
mkdirSync10(join21(planDir, "meta"), { recursive: true });
|
|
8811
8784
|
writePlanRemoteFiles(
|
|
8812
8785
|
planDir,
|
|
8813
8786
|
remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
|
|
@@ -8817,23 +8790,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8817
8790
|
}))
|
|
8818
8791
|
);
|
|
8819
8792
|
writeFileSync6(
|
|
8820
|
-
|
|
8793
|
+
join21(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
|
|
8821
8794
|
JSON.stringify(planVideosJson, null, 2),
|
|
8822
8795
|
"utf-8"
|
|
8823
8796
|
);
|
|
8824
8797
|
const digests = options.digests ?? /* @__PURE__ */ new Map();
|
|
8825
8798
|
const separateAudio = plansSeparateAudio(config, options);
|
|
8826
8799
|
const chunksCarryAudio = !separateAudio && carriesChunkAudio(config, options);
|
|
8827
|
-
const audioSeconds = chunksCarryAudio ? totalFrames / config.fps : probeResult.duration;
|
|
8828
|
-
const planMixPcmPath = join22(workDir, "plan-mix.wav");
|
|
8829
8800
|
stageStarted = Date.now();
|
|
8830
8801
|
const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
|
|
8831
|
-
separateAudio ? null : mixPlanAudio(
|
|
8832
|
-
c,
|
|
8833
|
-
finalCompiledDir,
|
|
8834
|
-
audioSeconds,
|
|
8835
|
-
chunksCarryAudio ? planMixPcmPath : void 0
|
|
8836
|
-
).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
8802
|
+
separateAudio ? null : mixPlanAudio(c, finalCompiledDir).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
8837
8803
|
(async () => {
|
|
8838
8804
|
for (const dir of [finalCompiledDir, videoFramesDst, videoSourcesDir]) {
|
|
8839
8805
|
await prehashPlanFiles(dir, digests);
|
|
@@ -8886,14 +8852,11 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8886
8852
|
if (audioResult?.audioError) {
|
|
8887
8853
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
8888
8854
|
}
|
|
8889
|
-
const planAudioPath =
|
|
8855
|
+
const planAudioPath = join21(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
8890
8856
|
if (audioResult?.hasAudio && existsSync15(audioResult.audioOutputPath)) {
|
|
8891
8857
|
renameSync4(audioResult.audioOutputPath, planAudioPath);
|
|
8892
8858
|
}
|
|
8893
8859
|
if (chunksCarryAudio && existsSync15(planAudioPath)) {
|
|
8894
|
-
stageStarted = Date.now();
|
|
8895
|
-
await holdPlanAudioPeak(c, planAudioPath, planMixPcmPath, audioSeconds);
|
|
8896
|
-
logPlanStage(log, "audio-peak", stageStarted);
|
|
8897
8860
|
if (options.sliceChunkAudio) {
|
|
8898
8861
|
stageStarted = Date.now();
|
|
8899
8862
|
await writeChunkAudioSlices({
|
|
@@ -8938,7 +8901,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8938
8901
|
format: config.format
|
|
8939
8902
|
};
|
|
8940
8903
|
try {
|
|
8941
|
-
|
|
8904
|
+
rmSync9(workDir, { recursive: true, force: true });
|
|
8942
8905
|
} catch (err) {
|
|
8943
8906
|
log.warn("[plan] failed to remove temp work dir", {
|
|
8944
8907
|
workDir,
|
|
@@ -8995,25 +8958,20 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
|
|
|
8995
8958
|
executionPlanSizeLimitBytes: Number.MAX_SAFE_INTEGER
|
|
8996
8959
|
});
|
|
8997
8960
|
try {
|
|
8998
|
-
const seconds = c.totalFrames / config.fps;
|
|
8999
|
-
const pcmPath = join22(c.workDir, "plan-mix.wav");
|
|
9000
8961
|
let stageStarted = Date.now();
|
|
9001
8962
|
await extractPlanVideos(c, true);
|
|
9002
8963
|
logPlanStage(c.log, "extract", stageStarted);
|
|
9003
8964
|
stageStarted = Date.now();
|
|
9004
|
-
const mixed = await mixPlanAudio(c, c.compiledDir
|
|
8965
|
+
const mixed = await mixPlanAudio(c, c.compiledDir);
|
|
9005
8966
|
logPlanStage(c.log, "audio", stageStarted);
|
|
9006
8967
|
if (mixed.audioError) {
|
|
9007
8968
|
applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
|
|
9008
8969
|
}
|
|
9009
8970
|
if (!mixed.hasAudio || !existsSync15(mixed.audioOutputPath)) return { hasAudio: false };
|
|
9010
8971
|
renameSync4(mixed.audioOutputPath, outputPath);
|
|
9011
|
-
stageStarted = Date.now();
|
|
9012
|
-
await holdPlanAudioPeak(c, outputPath, pcmPath, seconds);
|
|
9013
|
-
logPlanStage(c.log, "audio-peak", stageStarted);
|
|
9014
8972
|
return { hasAudio: true };
|
|
9015
8973
|
} finally {
|
|
9016
|
-
|
|
8974
|
+
rmSync9(c.workDir, { recursive: true, force: true });
|
|
9017
8975
|
}
|
|
9018
8976
|
}
|
|
9019
8977
|
async function plan(projectDir, config, planDir, options = {}) {
|
|
@@ -9043,12 +9001,12 @@ import {
|
|
|
9043
9001
|
readFileSync as readFileSync10,
|
|
9044
9002
|
readdirSync as readdirSync7,
|
|
9045
9003
|
renameSync as renameSync6,
|
|
9046
|
-
rmSync as
|
|
9004
|
+
rmSync as rmSync11,
|
|
9047
9005
|
statSync as statSync6,
|
|
9048
9006
|
writeFileSync as writeFileSync8
|
|
9049
9007
|
} from "node:fs";
|
|
9050
9008
|
import { tmpdir } from "node:os";
|
|
9051
|
-
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";
|
|
9052
9010
|
import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
9053
9011
|
import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
|
|
9054
9012
|
|
|
@@ -9060,11 +9018,11 @@ import {
|
|
|
9060
9018
|
mkdirSync as mkdirSync11,
|
|
9061
9019
|
mkdtempSync as mkdtempSync4,
|
|
9062
9020
|
renameSync as renameSync5,
|
|
9063
|
-
rmSync as
|
|
9021
|
+
rmSync as rmSync10,
|
|
9064
9022
|
statSync as statSync5,
|
|
9065
9023
|
writeFileSync as writeFileSync7
|
|
9066
9024
|
} from "node:fs";
|
|
9067
|
-
import { dirname as dirname10, join as
|
|
9025
|
+
import { dirname as dirname10, join as join23 } from "node:path";
|
|
9068
9026
|
|
|
9069
9027
|
// src/services/distributed/planV2Errors.ts
|
|
9070
9028
|
var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
|
|
@@ -9079,7 +9037,7 @@ var PlanV2IntegrityError = class extends Error {
|
|
|
9079
9037
|
};
|
|
9080
9038
|
|
|
9081
9039
|
// src/services/distributed/planV2Layout.ts
|
|
9082
|
-
import { join as
|
|
9040
|
+
import { join as join22 } from "node:path";
|
|
9083
9041
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
9084
9042
|
function assertPlanV2Sha256(value, field) {
|
|
9085
9043
|
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
@@ -9089,7 +9047,7 @@ function assertPlanV2Sha256(value, field) {
|
|
|
9089
9047
|
}
|
|
9090
9048
|
function planV2BlobPath(root, sha256) {
|
|
9091
9049
|
const digest = assertPlanV2Sha256(sha256, "artifact sha256");
|
|
9092
|
-
return
|
|
9050
|
+
return join22(root, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
9093
9051
|
}
|
|
9094
9052
|
|
|
9095
9053
|
// src/services/distributed/planV2Publisher.ts
|
|
@@ -9130,7 +9088,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9130
9088
|
this.destinationDir = destinationDir;
|
|
9131
9089
|
this.#linkFile = options.linkFile ?? linkSync2;
|
|
9132
9090
|
mkdirSync11(dirname10(destinationDir), { recursive: true });
|
|
9133
|
-
this.temporaryDir = mkdtempSync4(
|
|
9091
|
+
this.temporaryDir = mkdtempSync4(join23(dirname10(destinationDir), ".plan-v2-publish-"));
|
|
9134
9092
|
}
|
|
9135
9093
|
async putBlob(blob) {
|
|
9136
9094
|
const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
|
|
@@ -9143,8 +9101,8 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9143
9101
|
const destinationPath = planV2BlobPath(this.temporaryDir, digest);
|
|
9144
9102
|
if (existsSync16(destinationPath)) return;
|
|
9145
9103
|
mkdirSync11(dirname10(destinationPath), { recursive: true });
|
|
9146
|
-
const stagingDir = mkdtempSync4(
|
|
9147
|
-
const temporaryPath =
|
|
9104
|
+
const stagingDir = mkdtempSync4(join23(dirname10(destinationPath), ".plan-v2-blob-"));
|
|
9105
|
+
const temporaryPath = join23(stagingDir, "blob");
|
|
9148
9106
|
try {
|
|
9149
9107
|
try {
|
|
9150
9108
|
this.#linkFile(blob.sourcePath, temporaryPath);
|
|
@@ -9154,7 +9112,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9154
9112
|
}
|
|
9155
9113
|
renameSync5(temporaryPath, destinationPath);
|
|
9156
9114
|
} finally {
|
|
9157
|
-
|
|
9115
|
+
rmSync10(stagingDir, { recursive: true, force: true });
|
|
9158
9116
|
}
|
|
9159
9117
|
}
|
|
9160
9118
|
async commitManifest(manifestBytes) {
|
|
@@ -9165,13 +9123,13 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9165
9123
|
);
|
|
9166
9124
|
}
|
|
9167
9125
|
}
|
|
9168
|
-
writeFileSync7(
|
|
9126
|
+
writeFileSync7(join23(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
|
|
9169
9127
|
renameSync5(this.temporaryDir, this.destinationDir);
|
|
9170
9128
|
this.#committed = true;
|
|
9171
9129
|
}
|
|
9172
9130
|
async abort() {
|
|
9173
9131
|
if (!this.#committed) {
|
|
9174
|
-
|
|
9132
|
+
rmSync10(this.temporaryDir, { recursive: true, force: true });
|
|
9175
9133
|
}
|
|
9176
9134
|
}
|
|
9177
9135
|
};
|
|
@@ -9210,7 +9168,7 @@ function listFiles(root) {
|
|
|
9210
9168
|
const rootResolved = resolve8(root);
|
|
9211
9169
|
function walk(dir) {
|
|
9212
9170
|
for (const entry of readdirSync7(dir, { withFileTypes: true })) {
|
|
9213
|
-
const absolutePath =
|
|
9171
|
+
const absolutePath = join24(dir, entry.name);
|
|
9214
9172
|
if (entry.isDirectory()) {
|
|
9215
9173
|
walk(absolutePath);
|
|
9216
9174
|
} else if (entry.isFile()) {
|
|
@@ -9237,13 +9195,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
|
|
|
9237
9195
|
}
|
|
9238
9196
|
}
|
|
9239
9197
|
function validateExtractionCacheCompleteSentinels(executionPlanDir) {
|
|
9240
|
-
const videoRoot =
|
|
9198
|
+
const videoRoot = join24(executionPlanDir, "video-frames");
|
|
9241
9199
|
if (!existsSync17(videoRoot)) return;
|
|
9242
9200
|
for (const videoEntry of readdirSync7(videoRoot, { withFileTypes: true })) {
|
|
9243
9201
|
if (!videoEntry.isDirectory()) continue;
|
|
9244
|
-
const videoDir =
|
|
9202
|
+
const videoDir = join24(videoRoot, videoEntry.name);
|
|
9245
9203
|
if (readdirSync7(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
|
|
9246
|
-
const sentinelPath =
|
|
9204
|
+
const sentinelPath = join24(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
|
|
9247
9205
|
assertValidExtractionCacheCompleteSentinel(sentinelPath);
|
|
9248
9206
|
}
|
|
9249
9207
|
}
|
|
@@ -9290,7 +9248,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
9290
9248
|
if (video.deferredRange) {
|
|
9291
9249
|
const framePaths2 = /* @__PURE__ */ new Map();
|
|
9292
9250
|
for (let index = 0; index < video.totalFrames; index++) {
|
|
9293
|
-
framePaths2.set(index,
|
|
9251
|
+
framePaths2.set(index, join24(outputDir, String(index)));
|
|
9294
9252
|
}
|
|
9295
9253
|
return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
|
|
9296
9254
|
}
|
|
@@ -9298,7 +9256,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
9298
9256
|
const framePaths = /* @__PURE__ */ new Map();
|
|
9299
9257
|
for (const frameName of frameNames) {
|
|
9300
9258
|
if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
|
|
9301
|
-
assertValidExtractionCacheCompleteSentinel(
|
|
9259
|
+
assertValidExtractionCacheCompleteSentinel(join24(outputDir, frameName));
|
|
9302
9260
|
continue;
|
|
9303
9261
|
}
|
|
9304
9262
|
const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
|
|
@@ -9317,7 +9275,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
9317
9275
|
`duplicate extracted frame index ${frameIndex} in ${outputDir}`
|
|
9318
9276
|
);
|
|
9319
9277
|
}
|
|
9320
|
-
framePaths.set(frameIndex,
|
|
9278
|
+
framePaths.set(frameIndex, join24(outputDir, frameName));
|
|
9321
9279
|
}
|
|
9322
9280
|
return {
|
|
9323
9281
|
...video,
|
|
@@ -9338,7 +9296,7 @@ function parsePlanVideosJson2(value) {
|
|
|
9338
9296
|
}
|
|
9339
9297
|
}
|
|
9340
9298
|
function materializeExtractedVideoDirectories(planDir) {
|
|
9341
|
-
const videosPath =
|
|
9299
|
+
const videosPath = join24(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
9342
9300
|
if (!existsSync17(videosPath)) return;
|
|
9343
9301
|
const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
|
|
9344
9302
|
for (const video of videos.extracted) {
|
|
@@ -9367,13 +9325,13 @@ function parseChunkSlices(value) {
|
|
|
9367
9325
|
});
|
|
9368
9326
|
}
|
|
9369
9327
|
function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
9370
|
-
const videoRoot =
|
|
9328
|
+
const videoRoot = join24(executionPlanDir, "video-frames");
|
|
9371
9329
|
const hasExtractedFrames = existsSync17(videoRoot) && listFiles(videoRoot).length > 0;
|
|
9372
|
-
const videosPath =
|
|
9330
|
+
const videosPath = join24(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
9373
9331
|
if (!existsSync17(videosPath)) {
|
|
9374
9332
|
return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
|
|
9375
9333
|
}
|
|
9376
|
-
const chunksPath =
|
|
9334
|
+
const chunksPath = join24(executionPlanDir, "meta", "chunks.json");
|
|
9377
9335
|
const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
9378
9336
|
const chunks = readJsonFile(chunksPath, "meta/chunks.json");
|
|
9379
9337
|
const parsedVideos = parsePlanVideosJson2(videos);
|
|
@@ -9429,13 +9387,13 @@ function computeManifestHash(manifest) {
|
|
|
9429
9387
|
function writeBlob(sourcePath, destinationPath) {
|
|
9430
9388
|
if (existsSync17(destinationPath)) return;
|
|
9431
9389
|
mkdirSync12(dirname11(destinationPath), { recursive: true });
|
|
9432
|
-
const temporaryDir = mkdtempSync5(
|
|
9433
|
-
const temporaryPath =
|
|
9390
|
+
const temporaryDir = mkdtempSync5(join24(dirname11(destinationPath), ".plan-v2-blob-"));
|
|
9391
|
+
const temporaryPath = join24(temporaryDir, "blob");
|
|
9434
9392
|
try {
|
|
9435
9393
|
copyFileSync7(sourcePath, temporaryPath);
|
|
9436
9394
|
renameSync6(temporaryPath, destinationPath);
|
|
9437
9395
|
} finally {
|
|
9438
|
-
|
|
9396
|
+
rmSync11(temporaryDir, { recursive: true, force: true });
|
|
9439
9397
|
}
|
|
9440
9398
|
}
|
|
9441
9399
|
function publishBlobFor(absolutePath, digests) {
|
|
@@ -9466,7 +9424,7 @@ async function putTasks(tasks) {
|
|
|
9466
9424
|
if (failure) throw failure.reason;
|
|
9467
9425
|
}
|
|
9468
9426
|
function buildPlanV2Publication(executionPlanDir, digests) {
|
|
9469
|
-
const executionPlanPath =
|
|
9427
|
+
const executionPlanPath = join24(executionPlanDir, "plan.json");
|
|
9470
9428
|
if (!existsSync17(executionPlanPath)) {
|
|
9471
9429
|
throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
|
|
9472
9430
|
}
|
|
@@ -9494,7 +9452,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
9494
9452
|
}
|
|
9495
9453
|
validateExtractionCacheCompleteSentinels(executionPlanDir);
|
|
9496
9454
|
const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
|
|
9497
|
-
const encoder = readJsonFile(
|
|
9455
|
+
const encoder = readJsonFile(join24(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
|
|
9498
9456
|
const files = listFiles(executionPlanDir);
|
|
9499
9457
|
const chunkAudio = !isRecord3(encoder) || encoder.chunksCarryAudio !== true ? "none" : files.some((file) => chunkAudioSliceTarget(file.path) !== null) ? "sliced" : "whole";
|
|
9500
9458
|
for (const file of files) {
|
|
@@ -9555,20 +9513,20 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
|
9555
9513
|
throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
|
|
9556
9514
|
}
|
|
9557
9515
|
mkdirSync12(dirname11(planV2Dir), { recursive: true });
|
|
9558
|
-
const tempDir = mkdtempSync5(
|
|
9516
|
+
const tempDir = mkdtempSync5(join24(dirname11(planV2Dir), ".plan-v2-build-"));
|
|
9559
9517
|
try {
|
|
9560
9518
|
for (const blob of publication.blobs) {
|
|
9561
9519
|
writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
|
|
9562
9520
|
}
|
|
9563
9521
|
writeFileSync8(
|
|
9564
|
-
|
|
9522
|
+
join24(tempDir, "plan.json"),
|
|
9565
9523
|
canonicalJsonStringify(publication.manifest),
|
|
9566
9524
|
"utf-8"
|
|
9567
9525
|
);
|
|
9568
9526
|
renameSync6(tempDir, planV2Dir);
|
|
9569
9527
|
return resultFromManifest(planV2Dir, publication.manifest);
|
|
9570
9528
|
} catch (error) {
|
|
9571
|
-
|
|
9529
|
+
rmSync11(tempDir, { recursive: true, force: true });
|
|
9572
9530
|
throw error;
|
|
9573
9531
|
}
|
|
9574
9532
|
}
|
|
@@ -9608,7 +9566,7 @@ async function publishPlanV2FromV1(executionPlanDir, publisher) {
|
|
|
9608
9566
|
return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
|
|
9609
9567
|
}
|
|
9610
9568
|
async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
|
|
9611
|
-
const stagingRoot = mkdtempSync5(
|
|
9569
|
+
const stagingRoot = mkdtempSync5(join24(options.stagingParentDir ?? tmpdir(), ".execution-plan-"));
|
|
9612
9570
|
const digests = /* @__PURE__ */ new Map();
|
|
9613
9571
|
try {
|
|
9614
9572
|
await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
|
|
@@ -9619,7 +9577,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
9619
9577
|
// artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
|
|
9620
9578
|
whileAudioMixes: () => putBlobs(
|
|
9621
9579
|
publisher,
|
|
9622
|
-
listFiles(
|
|
9580
|
+
listFiles(join24(stagingRoot, "compiled")).map(
|
|
9623
9581
|
(file) => publishBlobFor(file.absolutePath, digests)
|
|
9624
9582
|
)
|
|
9625
9583
|
),
|
|
@@ -9635,7 +9593,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
9635
9593
|
}
|
|
9636
9594
|
throw error;
|
|
9637
9595
|
} finally {
|
|
9638
|
-
|
|
9596
|
+
rmSync11(stagingRoot, { recursive: true, force: true });
|
|
9639
9597
|
}
|
|
9640
9598
|
}
|
|
9641
9599
|
async function planV2(projectDir, config, planV2Dir) {
|
|
@@ -9651,7 +9609,7 @@ async function planV2(projectDir, config, planV2Dir) {
|
|
|
9651
9609
|
function resultFromManifest(planV2Dir, manifest) {
|
|
9652
9610
|
return {
|
|
9653
9611
|
planDir: planV2Dir,
|
|
9654
|
-
manifestPath:
|
|
9612
|
+
manifestPath: join24(planV2Dir, "plan.json"),
|
|
9655
9613
|
planProtocol: PLAN_PROTOCOL_V2,
|
|
9656
9614
|
planHash: manifest.planHash,
|
|
9657
9615
|
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
@@ -9787,7 +9745,7 @@ function parsePlanV2Manifest(value) {
|
|
|
9787
9745
|
return parsed;
|
|
9788
9746
|
}
|
|
9789
9747
|
function readPlanV2Manifest(planV2Dir) {
|
|
9790
|
-
const manifestPath =
|
|
9748
|
+
const manifestPath = join24(planV2Dir, "plan.json");
|
|
9791
9749
|
if (!existsSync17(manifestPath)) {
|
|
9792
9750
|
throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
|
|
9793
9751
|
}
|
|
@@ -9833,16 +9791,16 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
9833
9791
|
sourcePath: verifyBlob(planV2Dir, artifact)
|
|
9834
9792
|
}));
|
|
9835
9793
|
mkdirSync12(dirname11(destinationDir), { recursive: true });
|
|
9836
|
-
const tempDir = mkdtempSync5(
|
|
9794
|
+
const tempDir = mkdtempSync5(join24(dirname11(destinationDir), ".plan-v2-materialize-"));
|
|
9837
9795
|
try {
|
|
9838
9796
|
for (const { artifact, sourcePath } of verified) {
|
|
9839
|
-
const destinationPath =
|
|
9797
|
+
const destinationPath = join24(tempDir, ...artifact.path.split("/"));
|
|
9840
9798
|
mkdirSync12(dirname11(destinationPath), { recursive: true });
|
|
9841
9799
|
copyFileSync7(sourcePath, destinationPath);
|
|
9842
9800
|
}
|
|
9843
9801
|
if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
|
|
9844
9802
|
writeFileSync8(
|
|
9845
|
-
|
|
9803
|
+
join24(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
|
|
9846
9804
|
canonicalJsonStringify({
|
|
9847
9805
|
manifest,
|
|
9848
9806
|
target,
|
|
@@ -9852,7 +9810,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
9852
9810
|
);
|
|
9853
9811
|
renameSync6(tempDir, destinationDir);
|
|
9854
9812
|
} catch (error) {
|
|
9855
|
-
|
|
9813
|
+
rmSync11(tempDir, { recursive: true, force: true });
|
|
9856
9814
|
throw error;
|
|
9857
9815
|
}
|
|
9858
9816
|
return {
|
|
@@ -9865,7 +9823,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
9865
9823
|
// Join the artifact's OWN name, not the current constant: a plan written
|
|
9866
9824
|
// before the container change carries the legacy name, and materializing it
|
|
9867
9825
|
// under the new one would point at a file that was never written.
|
|
9868
|
-
audioPath: target.role === "assembler" ? audioArtifact ?
|
|
9826
|
+
audioPath: target.role === "assembler" ? audioArtifact ? join24(destinationDir, audioArtifact.path) : null : null
|
|
9869
9827
|
};
|
|
9870
9828
|
}
|
|
9871
9829
|
function assertRemoteSources(remoteSources, artifacts) {
|
|
@@ -9884,7 +9842,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
|
9884
9842
|
return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
|
|
9885
9843
|
}
|
|
9886
9844
|
function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
9887
|
-
const markerPath =
|
|
9845
|
+
const markerPath = join24(planDir, PLAN_V2_MATERIALIZATION_MARKER);
|
|
9888
9846
|
if (!existsSync17(markerPath)) return null;
|
|
9889
9847
|
const marker = readJsonFile(markerPath, "materialization marker");
|
|
9890
9848
|
if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
|
|
@@ -9902,7 +9860,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
9902
9860
|
assertRemoteSources(remoteSources, required);
|
|
9903
9861
|
for (const artifact of required) {
|
|
9904
9862
|
if (Object.hasOwn(remoteSources, artifact.path)) continue;
|
|
9905
|
-
const path =
|
|
9863
|
+
const path = join24(planDir, ...artifact.path.split("/"));
|
|
9906
9864
|
if (!existsSync17(path) || statSync6(path).size !== artifact.sizeBytes) {
|
|
9907
9865
|
throw new PlanV2IntegrityError(
|
|
9908
9866
|
`materialized artifact missing or truncated: ${artifact.path}`
|
|
@@ -9916,9 +9874,9 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
9916
9874
|
}
|
|
9917
9875
|
|
|
9918
9876
|
// src/services/distributed/planV2Execution.ts
|
|
9919
|
-
import { mkdtempSync as mkdtempSync7, rmSync as
|
|
9877
|
+
import { mkdtempSync as mkdtempSync7, rmSync as rmSync14 } from "node:fs";
|
|
9920
9878
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
9921
|
-
import { join as
|
|
9879
|
+
import { join as join27 } from "node:path";
|
|
9922
9880
|
|
|
9923
9881
|
// src/services/distributed/assemble.ts
|
|
9924
9882
|
import {
|
|
@@ -9929,11 +9887,11 @@ import {
|
|
|
9929
9887
|
readFileSync as readFileSync11,
|
|
9930
9888
|
readdirSync as readdirSync8,
|
|
9931
9889
|
renameSync as renameSync7,
|
|
9932
|
-
rmSync as
|
|
9890
|
+
rmSync as rmSync12,
|
|
9933
9891
|
statSync as statSync7,
|
|
9934
9892
|
writeFileSync as writeFileSync9
|
|
9935
9893
|
} from "node:fs";
|
|
9936
|
-
import { dirname as dirname12, join as
|
|
9894
|
+
import { dirname as dirname12, join as join25 } from "node:path";
|
|
9937
9895
|
import {
|
|
9938
9896
|
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
9939
9897
|
describeFfmpegFailure as describeFfmpegFailure4,
|
|
@@ -9941,7 +9899,8 @@ import {
|
|
|
9941
9899
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
|
|
9942
9900
|
muxVideoWithAudio as muxVideoWithAudio2,
|
|
9943
9901
|
resolveConfig as resolveConfig4,
|
|
9944
|
-
runFfmpeg as runFfmpeg7
|
|
9902
|
+
runFfmpeg as runFfmpeg7,
|
|
9903
|
+
appendSoftwareEncoderThreadsArg
|
|
9945
9904
|
} from "@hyperframes/engine";
|
|
9946
9905
|
import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
|
|
9947
9906
|
function cfrRateControlArgs(locked) {
|
|
@@ -9995,7 +9954,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
9995
9954
|
};
|
|
9996
9955
|
try {
|
|
9997
9956
|
const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
|
|
9998
|
-
const finalPath =
|
|
9957
|
+
const finalPath = join25(workDir, `output.${format}`);
|
|
9999
9958
|
const audio = audioPath !== null && existsSync18(audioPath) ? audioPath : null;
|
|
10000
9959
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
10001
9960
|
renameSync7(finalPath, outputPath);
|
|
@@ -10006,7 +9965,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
10006
9965
|
});
|
|
10007
9966
|
} finally {
|
|
10008
9967
|
try {
|
|
10009
|
-
|
|
9968
|
+
rmSync12(workDir, { recursive: true, force: true });
|
|
10010
9969
|
} catch (err) {
|
|
10011
9970
|
log.warn("[assemble] failed to remove work dir", {
|
|
10012
9971
|
workDir,
|
|
@@ -10023,8 +9982,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
10023
9982
|
};
|
|
10024
9983
|
}
|
|
10025
9984
|
function readAssemblePlan(planDir, chunkPaths) {
|
|
10026
|
-
const planJsonPath =
|
|
10027
|
-
const chunksJsonPath =
|
|
9985
|
+
const planJsonPath = join25(planDir, "plan.json");
|
|
9986
|
+
const chunksJsonPath = join25(planDir, "meta", "chunks.json");
|
|
10028
9987
|
if (!existsSync18(planJsonPath)) {
|
|
10029
9988
|
throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
|
|
10030
9989
|
}
|
|
@@ -10055,10 +10014,9 @@ function readAssemblePlan(planDir, chunkPaths) {
|
|
|
10055
10014
|
function videoInputArgs(ctx, videoSources) {
|
|
10056
10015
|
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
|
|
10057
10016
|
}
|
|
10058
|
-
|
|
10059
|
-
const
|
|
10060
|
-
|
|
10061
|
-
...videoInputArgs(ctx, chunkPaths),
|
|
10017
|
+
function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
|
|
10018
|
+
const args = [
|
|
10019
|
+
...inputArgs,
|
|
10062
10020
|
"-c:v",
|
|
10063
10021
|
"libx264",
|
|
10064
10022
|
...cfrRateControlArgs(encoder),
|
|
@@ -10067,8 +10025,14 @@ async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
|
10067
10025
|
"-fps_mode",
|
|
10068
10026
|
"cfr",
|
|
10069
10027
|
"-r",
|
|
10070
|
-
|
|
10028
|
+
fpsArg
|
|
10071
10029
|
];
|
|
10030
|
+
appendSoftwareEncoderThreadsArg(args);
|
|
10031
|
+
return args;
|
|
10032
|
+
}
|
|
10033
|
+
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
10034
|
+
const cfrOutputPath = join25(ctx.workDir, `cfr.${ctx.format}`);
|
|
10035
|
+
const cfrArgs = cfrEncodeArgs(videoInputArgs(ctx, chunkPaths), encoder, ctx.fpsArg);
|
|
10072
10036
|
appendRenderProvenanceArgs2(cfrArgs, cfrOutputPath);
|
|
10073
10037
|
cfrArgs.push("-y", cfrOutputPath);
|
|
10074
10038
|
await ctx.timed(
|
|
@@ -10094,7 +10058,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
10094
10058
|
);
|
|
10095
10059
|
return;
|
|
10096
10060
|
}
|
|
10097
|
-
const paddedAudioPath =
|
|
10061
|
+
const paddedAudioPath = join25(ctx.workDir, "audio-padded.m4a");
|
|
10098
10062
|
const padTrimResult = await ctx.timed(
|
|
10099
10063
|
"audioMs",
|
|
10100
10064
|
() => padOrTrimAudioToVideoFrameCount({
|
|
@@ -10134,7 +10098,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
10134
10098
|
}
|
|
10135
10099
|
}
|
|
10136
10100
|
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
10137
|
-
const listPath =
|
|
10101
|
+
const listPath = join25(workDir, "concat-list.txt");
|
|
10138
10102
|
const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
|
|
10139
10103
|
writeFileSync9(listPath, `${body}
|
|
10140
10104
|
`, "utf-8");
|
|
@@ -10162,6 +10126,7 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
|
10162
10126
|
function chunksAreStitchedMp4(planDir, cfr) {
|
|
10163
10127
|
const { chunksCarryAudio, separateAudio } = lockedAudio(planDir);
|
|
10164
10128
|
if (!chunksCarryAudio && !separateAudio) return false;
|
|
10129
|
+
if (planFormat(planDir) !== "mp4") return false;
|
|
10165
10130
|
if (cfr) {
|
|
10166
10131
|
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks are stitched");
|
|
10167
10132
|
}
|
|
@@ -10170,8 +10135,12 @@ function chunksAreStitchedMp4(planDir, cfr) {
|
|
|
10170
10135
|
function stitchesSeparateAudio(planDir) {
|
|
10171
10136
|
return lockedAudio(planDir).separateAudio === true;
|
|
10172
10137
|
}
|
|
10138
|
+
function planFormat(planDir) {
|
|
10139
|
+
const plan2 = JSON.parse(readFileSync11(join25(planDir, "plan.json"), "utf-8"));
|
|
10140
|
+
return plan2.dimensions.format;
|
|
10141
|
+
}
|
|
10173
10142
|
function lockedAudio(planDir) {
|
|
10174
|
-
const encoderJsonPath =
|
|
10143
|
+
const encoderJsonPath = join25(planDir, "meta", "encoder.json");
|
|
10175
10144
|
if (!existsSync18(encoderJsonPath)) return {};
|
|
10176
10145
|
return JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
|
|
10177
10146
|
}
|
|
@@ -10181,7 +10150,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
10181
10150
|
`[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.`
|
|
10182
10151
|
);
|
|
10183
10152
|
}
|
|
10184
|
-
const encoderJsonPath =
|
|
10153
|
+
const encoderJsonPath = join25(planDir, "meta", "encoder.json");
|
|
10185
10154
|
if (!existsSync18(encoderJsonPath)) {
|
|
10186
10155
|
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
|
|
10187
10156
|
}
|
|
@@ -10194,7 +10163,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
10194
10163
|
return encoderJson;
|
|
10195
10164
|
}
|
|
10196
10165
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
10197
|
-
if (existsSync18(outputPath))
|
|
10166
|
+
if (existsSync18(outputPath)) rmSync12(outputPath, { recursive: true, force: true });
|
|
10198
10167
|
mkdirSync13(outputPath, { recursive: true });
|
|
10199
10168
|
let globalIdx = 0;
|
|
10200
10169
|
for (const chunkDir of chunkPaths) {
|
|
@@ -10208,8 +10177,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
10208
10177
|
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
|
|
10209
10178
|
}
|
|
10210
10179
|
for (const frame of frames) {
|
|
10211
|
-
const dst =
|
|
10212
|
-
cpSync3(
|
|
10180
|
+
const dst = join25(outputPath, formatExportFrameName(globalIdx, "png"));
|
|
10181
|
+
cpSync3(join25(chunkDir, frame), dst);
|
|
10213
10182
|
globalIdx += 1;
|
|
10214
10183
|
}
|
|
10215
10184
|
}
|
|
@@ -10219,13 +10188,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
10219
10188
|
);
|
|
10220
10189
|
}
|
|
10221
10190
|
if (audioPath !== null && existsSync18(audioPath)) {
|
|
10222
|
-
const sidecar =
|
|
10191
|
+
const sidecar = join25(outputPath, MIXED_AUDIO_FILENAME4);
|
|
10223
10192
|
cpSync3(audioPath, sidecar);
|
|
10224
10193
|
}
|
|
10225
10194
|
let fileSize = 0;
|
|
10226
10195
|
for (const name of readdirSync8(outputPath)) {
|
|
10227
10196
|
try {
|
|
10228
|
-
fileSize += statSync7(
|
|
10197
|
+
fileSize += statSync7(join25(outputPath, name)).size;
|
|
10229
10198
|
} catch {
|
|
10230
10199
|
}
|
|
10231
10200
|
}
|
|
@@ -10239,8 +10208,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
10239
10208
|
|
|
10240
10209
|
// src/services/distributed/renderChunk.ts
|
|
10241
10210
|
import { randomBytes } from "node:crypto";
|
|
10242
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as
|
|
10243
|
-
import { join as
|
|
10211
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync13, writeFileSync as writeFileSync10 } from "node:fs";
|
|
10212
|
+
import { join as join26 } from "node:path";
|
|
10244
10213
|
import {
|
|
10245
10214
|
assertSwiftShader,
|
|
10246
10215
|
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
|
|
@@ -10342,7 +10311,7 @@ function hashChunkOutput(outputPath, kind) {
|
|
|
10342
10311
|
if (kind === "file") return sha256Hex(readFileSync12(outputPath));
|
|
10343
10312
|
const entries = readdirSync9(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
|
|
10344
10313
|
const lines = entries.map(
|
|
10345
|
-
(name) => `${name}\0${sha256Hex(readFileSync12(
|
|
10314
|
+
(name) => `${name}\0${sha256Hex(readFileSync12(join26(outputPath, name)))}`
|
|
10346
10315
|
);
|
|
10347
10316
|
return sha256Hex(lines.join("\0"));
|
|
10348
10317
|
}
|
|
@@ -10359,9 +10328,9 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
|
|
|
10359
10328
|
async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
10360
10329
|
const start = Date.now();
|
|
10361
10330
|
const log = defaultLogger;
|
|
10362
|
-
const planJsonPath =
|
|
10363
|
-
const encoderJsonPath =
|
|
10364
|
-
const chunksJsonPath =
|
|
10331
|
+
const planJsonPath = join26(planDir, "plan.json");
|
|
10332
|
+
const encoderJsonPath = join26(planDir, "meta", "encoder.json");
|
|
10333
|
+
const chunksJsonPath = join26(planDir, "meta", "chunks.json");
|
|
10365
10334
|
if (!existsSync19(planJsonPath)) {
|
|
10366
10335
|
throw new RenderChunkValidationError(
|
|
10367
10336
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -10387,7 +10356,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10387
10356
|
}
|
|
10388
10357
|
const encoder = JSON.parse(readFileSync12(encoderJsonPath, "utf-8"));
|
|
10389
10358
|
const chunks = JSON.parse(readFileSync12(chunksJsonPath, "utf-8"));
|
|
10390
|
-
const videosJsonPath =
|
|
10359
|
+
const videosJsonPath = join26(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
10391
10360
|
let planVideos = null;
|
|
10392
10361
|
if (existsSync19(videosJsonPath)) {
|
|
10393
10362
|
try {
|
|
@@ -10419,7 +10388,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10419
10388
|
}
|
|
10420
10389
|
const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
|
|
10421
10390
|
const framesInChunk = range.endFrame - range.startFrame;
|
|
10422
|
-
const compiledDir =
|
|
10391
|
+
const compiledDir = join26(planDir, "compiled");
|
|
10423
10392
|
if (!existsSync19(compiledDir)) {
|
|
10424
10393
|
throw new RenderChunkValidationError(
|
|
10425
10394
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -10477,7 +10446,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10477
10446
|
};
|
|
10478
10447
|
const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
10479
10448
|
mkdirSync14(workDir, { recursive: true });
|
|
10480
|
-
const framesDir =
|
|
10449
|
+
const framesDir = join26(workDir, "captured-frames");
|
|
10481
10450
|
mkdirSync14(framesDir, { recursive: true });
|
|
10482
10451
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
10483
10452
|
const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
|
|
@@ -10502,7 +10471,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10502
10471
|
dimensions: plan2.dimensions,
|
|
10503
10472
|
encoder,
|
|
10504
10473
|
videoCount: planVideos?.videos.length ?? 0,
|
|
10505
|
-
html: readFileSync12(
|
|
10474
|
+
html: readFileSync12(join26(compiledDir, "index.html"), "utf-8"),
|
|
10506
10475
|
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
10507
10476
|
skipReadinessVideoIds: deferredVideos.filter(
|
|
10508
10477
|
(video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
|
|
@@ -10678,7 +10647,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10678
10647
|
return observedMode;
|
|
10679
10648
|
},
|
|
10680
10649
|
resetForScreenshotRetry: () => {
|
|
10681
|
-
|
|
10650
|
+
rmSync13(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
10682
10651
|
mkdirSync14(framesDir, { recursive: true });
|
|
10683
10652
|
capturePerfs.length = 0;
|
|
10684
10653
|
captureAttempts.length = 0;
|
|
@@ -10716,7 +10685,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10716
10685
|
if (isPngSequence) {
|
|
10717
10686
|
if (!existsSync19(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
|
|
10718
10687
|
} else {
|
|
10719
|
-
const outDir =
|
|
10688
|
+
const outDir = join26(outputChunkPath, "..");
|
|
10720
10689
|
if (!existsSync19(outDir)) mkdirSync14(outDir, { recursive: true });
|
|
10721
10690
|
}
|
|
10722
10691
|
const encodeStarted = Date.now();
|
|
@@ -10771,8 +10740,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10771
10740
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
10772
10741
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
10773
10742
|
});
|
|
10774
|
-
|
|
10775
|
-
|
|
10743
|
+
rmSync13(videoOnlyPath, { force: true });
|
|
10744
|
+
rmSync13(audioScratchPath, { force: true });
|
|
10776
10745
|
}
|
|
10777
10746
|
encodeStageMs = Date.now() - encodeStarted;
|
|
10778
10747
|
} finally {
|
|
@@ -10818,8 +10787,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10818
10787
|
writeFileSync10(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
10819
10788
|
`, "utf-8");
|
|
10820
10789
|
try {
|
|
10821
|
-
|
|
10822
|
-
|
|
10790
|
+
rmSync13(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
10791
|
+
rmSync13(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
10823
10792
|
} catch (err) {
|
|
10824
10793
|
log.warn("[renderChunk] failed to remove work dir", {
|
|
10825
10794
|
workDir,
|
|
@@ -10848,18 +10817,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10848
10817
|
|
|
10849
10818
|
// src/services/distributed/planV2Execution.ts
|
|
10850
10819
|
async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
|
|
10851
|
-
const workRoot = mkdtempSync7(
|
|
10852
|
-
const materializedPlanDir =
|
|
10820
|
+
const workRoot = mkdtempSync7(join27(tmpdir2(), "hf-plan-v2-chunk-"));
|
|
10821
|
+
const materializedPlanDir = join27(workRoot, "plan");
|
|
10853
10822
|
try {
|
|
10854
10823
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
10855
10824
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
|
|
10856
10825
|
} finally {
|
|
10857
|
-
|
|
10826
|
+
rmSync14(workRoot, { recursive: true, force: true });
|
|
10858
10827
|
}
|
|
10859
10828
|
}
|
|
10860
10829
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
10861
|
-
const workRoot = mkdtempSync7(
|
|
10862
|
-
const materializedPlanDir =
|
|
10830
|
+
const workRoot = mkdtempSync7(join27(tmpdir2(), "hf-plan-v2-assembler-"));
|
|
10831
|
+
const materializedPlanDir = join27(workRoot, "plan");
|
|
10863
10832
|
try {
|
|
10864
10833
|
const materialized = materializePlanV2Target(
|
|
10865
10834
|
planV2Dir,
|
|
@@ -10874,7 +10843,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
10874
10843
|
options
|
|
10875
10844
|
);
|
|
10876
10845
|
} finally {
|
|
10877
|
-
|
|
10846
|
+
rmSync14(workRoot, { recursive: true, force: true });
|
|
10878
10847
|
}
|
|
10879
10848
|
}
|
|
10880
10849
|
|
|
@@ -10921,6 +10890,9 @@ function validateDistributedRenderConfig(config) {
|
|
|
10921
10890
|
}
|
|
10922
10891
|
validateIntDimension("config.width", config.width);
|
|
10923
10892
|
validateIntDimension("config.height", config.height);
|
|
10893
|
+
if (config.pageMotionBlur !== void 0 && config.pageMotionBlur !== "off") {
|
|
10894
|
+
throw new InvalidConfigError("config.pageMotionBlur", 'must be "off" when provided');
|
|
10895
|
+
}
|
|
10924
10896
|
if (!ALLOWED_FORMATS.includes(config.format)) {
|
|
10925
10897
|
throw new InvalidConfigError(
|
|
10926
10898
|
"config.format",
|
|
@@ -11123,7 +11095,7 @@ function validateIntDimension(field, value) {
|
|
|
11123
11095
|
// src/services/distributed/projectHash.ts
|
|
11124
11096
|
import { closeSync as closeSync5, openSync as openSync5, readdirSync as readdirSync10, readSync as readSync3, realpathSync as realpathSync2, statSync as statSync8 } from "node:fs";
|
|
11125
11097
|
import { createHash as createHash6 } from "node:crypto";
|
|
11126
|
-
import { join as
|
|
11098
|
+
import { join as join28 } from "node:path";
|
|
11127
11099
|
var SITE_ID_SCHEME = "site-v2\0";
|
|
11128
11100
|
var READ_CHUNK_BYTES = 256 * 1024;
|
|
11129
11101
|
function listSiteFiles(projectDir) {
|
|
@@ -11132,7 +11104,7 @@ function listSiteFiles(projectDir) {
|
|
|
11132
11104
|
function walk(dir, rel) {
|
|
11133
11105
|
for (const name of readdirSync10(dir).sort()) {
|
|
11134
11106
|
if (rel === "" && PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(name)) continue;
|
|
11135
|
-
const full =
|
|
11107
|
+
const full = join28(dir, name);
|
|
11136
11108
|
const relPath = rel === "" ? name : `${rel}/${name}`;
|
|
11137
11109
|
let stat;
|
|
11138
11110
|
try {
|
|
@@ -11162,7 +11134,7 @@ function hashProjectDir(projectDir, files = listSiteFiles(projectDir)) {
|
|
|
11162
11134
|
for (const rel of files) {
|
|
11163
11135
|
hash.update(rel);
|
|
11164
11136
|
hash.update("\0");
|
|
11165
|
-
const fd = openSync5(
|
|
11137
|
+
const fd = openSync5(join28(projectDir, rel), "r");
|
|
11166
11138
|
try {
|
|
11167
11139
|
let read;
|
|
11168
11140
|
while ((read = readSync3(fd, chunk, 0, chunk.length, null)) > 0) {
|