@hyperframes/producer 0.8.206 → 0.8.207
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/distributed.js +185 -96
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +74 -69
- package/dist/index.js +513 -309
- package/dist/public-server.js +450 -278
- package/dist/renderRequest.d.ts +3 -0
- package/dist/server.d.ts +3 -0
- package/dist/services/distributed/assemble.d.ts +1 -1
- package/dist/services/distributed/plan.d.ts +6 -0
- package/dist/services/render/deliveryEncoder.d.ts +22 -0
- package/dist/services/render/hlsConfig.d.ts +1 -1
- package/dist/services/render/segmentManifest.d.ts +3 -0
- package/dist/services/render/stages/captureHdrStage.d.ts +2 -0
- package/dist/services/render/stages/encodeStage.d.ts +2 -0
- package/dist/services/render/stages/freezePlan.d.ts +3 -0
- package/dist/services/render/stages/probeStage.d.ts +2 -3
- package/dist/services/renderOrchestrator.d.ts +6 -0
- package/package.json +7 -7
package/dist/public-server.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
statSync as statSync10,
|
|
8
8
|
mkdtempSync as mkdtempSync4,
|
|
9
9
|
writeFileSync as writeFileSync7,
|
|
10
|
-
rmSync as
|
|
10
|
+
rmSync as rmSync11,
|
|
11
11
|
createReadStream as createReadStream3
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import { resolve as resolve10, dirname as dirname13, join as join31 } from "node:path";
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
mkdirSync as mkdirSync10,
|
|
27
27
|
readFileSync as readFileSync9,
|
|
28
28
|
readdirSync as readdirSync5,
|
|
29
|
-
rmSync as
|
|
29
|
+
rmSync as rmSync10,
|
|
30
30
|
statSync as statSync8,
|
|
31
31
|
writeFileSync as writeFileSync6,
|
|
32
32
|
copyFileSync as copyFileSync5,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
36
36
|
import {
|
|
37
37
|
chainCapture,
|
|
38
|
-
fpsToNumber as
|
|
38
|
+
fpsToNumber as fpsToNumber8,
|
|
39
39
|
HF_COLOR_GRADING_ATTR,
|
|
40
40
|
HF_VFX_ATTR,
|
|
41
41
|
parseVfxChain,
|
|
@@ -831,7 +831,7 @@ function outputUsesH264Pipeline(format) {
|
|
|
831
831
|
}
|
|
832
832
|
|
|
833
833
|
// src/services/render/hlsConfig.ts
|
|
834
|
-
import {
|
|
834
|
+
import { keyframeIntervalToGopSize } from "@hyperframes/engine";
|
|
835
835
|
var DEFAULT_HLS_SEGMENT_SECONDS = 4;
|
|
836
836
|
function validateHlsRenderConfig(config) {
|
|
837
837
|
if (config.format !== "hls") return;
|
|
@@ -856,7 +856,7 @@ function resolveHlsSegmentSeconds(config) {
|
|
|
856
856
|
return config.hlsSegmentSeconds ?? DEFAULT_HLS_SEGMENT_SECONDS;
|
|
857
857
|
}
|
|
858
858
|
function resolveHlsGopSize(fps, segmentSeconds) {
|
|
859
|
-
return
|
|
859
|
+
return keyframeIntervalToGopSize(fps, segmentSeconds);
|
|
860
860
|
}
|
|
861
861
|
function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
|
|
862
862
|
if (outputFormat !== "hls") return { lockGopForChunkConcat: false, gopSize: void 0 };
|
|
@@ -866,6 +866,38 @@ function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
|
|
|
866
866
|
};
|
|
867
867
|
}
|
|
868
868
|
|
|
869
|
+
// src/services/render/deliveryEncoder.ts
|
|
870
|
+
import { appendRateCapArgs, keyframeIntervalToGopSize as keyframeIntervalToGopSize2 } from "@hyperframes/engine";
|
|
871
|
+
function validateDeliveryEncoderConfig(config) {
|
|
872
|
+
const { keyframeIntervalSeconds, maxBitrate, bufferSize, format } = config;
|
|
873
|
+
if (keyframeIntervalSeconds === void 0 && maxBitrate === void 0 && bufferSize === void 0)
|
|
874
|
+
return;
|
|
875
|
+
if (keyframeIntervalSeconds !== void 0) {
|
|
876
|
+
if (!Number.isFinite(keyframeIntervalSeconds) || keyframeIntervalSeconds <= 0) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
`keyframeIntervalSeconds must be a positive number of seconds; got ${keyframeIntervalSeconds}`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (format === "hls") {
|
|
882
|
+
throw new Error(
|
|
883
|
+
"keyframeIntervalSeconds does not apply to hls; hlsSegmentSeconds sets its keyframes"
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (format !== void 0 && format !== "mp4") {
|
|
888
|
+
throw new Error(`keyframeIntervalSeconds and maxBitrate apply to mp4 renders, not ${format}`);
|
|
889
|
+
}
|
|
890
|
+
appendRateCapArgs([], { maxBitrate, bufferSize });
|
|
891
|
+
}
|
|
892
|
+
function resolveDeliveryEncoderOptions(config) {
|
|
893
|
+
const { maxBitrate, bufferSize, keyframeIntervalSeconds } = config;
|
|
894
|
+
const gop = keyframeIntervalSeconds === void 0 ? resolveHlsEncoderGopLock(config.format ?? "mp4", config.fps, config.hlsSegmentSeconds) : {
|
|
895
|
+
lockGopForChunkConcat: true,
|
|
896
|
+
gopSize: keyframeIntervalToGopSize2(config.fps, keyframeIntervalSeconds)
|
|
897
|
+
};
|
|
898
|
+
return { maxBitrate, bufferSize, ...gop };
|
|
899
|
+
}
|
|
900
|
+
|
|
869
901
|
// src/services/render/shared.ts
|
|
870
902
|
import {
|
|
871
903
|
copyFileSync,
|
|
@@ -1884,7 +1916,7 @@ function replanSegmentedAfterFailure(plan, failure) {
|
|
|
1884
1916
|
const keepSegmenting = failure.kind === "draw_element_verification" || failure.kind === "draw_element_capture";
|
|
1885
1917
|
return createCapturePlan({
|
|
1886
1918
|
...plan,
|
|
1887
|
-
forceScreenshot: keepSegmenting || failure.kind === "capture_failure",
|
|
1919
|
+
forceScreenshot: plan.forceScreenshot || keepSegmenting || failure.kind === "capture_failure",
|
|
1888
1920
|
useStreamingEncode: true,
|
|
1889
1921
|
useLayeredComposite: false,
|
|
1890
1922
|
useSegmentedCapture: keepSegmenting,
|
|
@@ -1954,7 +1986,7 @@ import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
|
|
|
1954
1986
|
|
|
1955
1987
|
// src/services/render/perfSummary.ts
|
|
1956
1988
|
import { arch, cpus, platform, totalmem } from "node:os";
|
|
1957
|
-
import { fpsToNumber as
|
|
1989
|
+
import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
|
|
1958
1990
|
|
|
1959
1991
|
// src/services/render/hdrPerf.ts
|
|
1960
1992
|
function createHdrPerfCollector() {
|
|
@@ -2225,7 +2257,7 @@ function buildRenderPerfSummary(input) {
|
|
|
2225
2257
|
// payloads (CLI telemetry, regression-harness reports) where a single
|
|
2226
2258
|
// number is friendlier than `{num,den}`. Callers needing the rational
|
|
2227
2259
|
// back can read `job.config.fps`.
|
|
2228
|
-
fps:
|
|
2260
|
+
fps: fpsToNumber2(input.job.config.fps),
|
|
2229
2261
|
quality: input.job.config.quality,
|
|
2230
2262
|
workers: input.workerCount,
|
|
2231
2263
|
workerSizing: input.workerSizing,
|
|
@@ -2810,7 +2842,7 @@ import {
|
|
|
2810
2842
|
resolveVideoExtractionWindow,
|
|
2811
2843
|
runFfmpeg
|
|
2812
2844
|
} from "@hyperframes/engine";
|
|
2813
|
-
import { fpsToFfmpegArg, fpsToNumber as
|
|
2845
|
+
import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
|
|
2814
2846
|
|
|
2815
2847
|
// src/services/hdrFrameWindows.ts
|
|
2816
2848
|
import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
|
|
@@ -3113,7 +3145,7 @@ async function extractHdrVideoFrames(args) {
|
|
|
3113
3145
|
const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
|
|
3114
3146
|
const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
|
|
3115
3147
|
const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
|
|
3116
|
-
const fps =
|
|
3148
|
+
const fps = fpsToNumber3(job.config.fps);
|
|
3117
3149
|
const out = /* @__PURE__ */ new Map();
|
|
3118
3150
|
mkdirSync2(framesDir, { recursive: true });
|
|
3119
3151
|
let scratchBytes = 0;
|
|
@@ -3951,7 +3983,6 @@ async function runCaptureStreamingStage(input) {
|
|
|
3951
3983
|
const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
|
|
3952
3984
|
if (workerCount > 1) {
|
|
3953
3985
|
const deParallelStream = forceParallelStream === true || process.env.HF_DE_PARALLEL_STREAM === "true";
|
|
3954
|
-
const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames, workerCount, workDir) : distributeFrames(totalFrames, workerCount, workDir);
|
|
3955
3986
|
const parallelStats = {
|
|
3956
3987
|
verifyChecked: 0,
|
|
3957
3988
|
blankSuspects: 0,
|
|
@@ -3964,143 +3995,158 @@ async function runCaptureStreamingStage(input) {
|
|
|
3964
3995
|
frameTime: (i) => i * job.config.fps.den / job.config.fps.num
|
|
3965
3996
|
});
|
|
3966
3997
|
let parallelGuardRan = false;
|
|
3967
|
-
let
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3998
|
+
let writtenFrames = 0;
|
|
3999
|
+
let resumeFrom = 0;
|
|
4000
|
+
let resumedAfterStall = false;
|
|
4001
|
+
let workerResults;
|
|
4002
|
+
for (; ; ) {
|
|
4003
|
+
const parallelReorder = createFrameReorderBuffer(resumeFrom, totalFrames);
|
|
4004
|
+
const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames - resumeFrom, workerCount, workDir, resumeFrom) : distributeFrames(totalFrames - resumeFrom, workerCount, workDir, resumeFrom);
|
|
4005
|
+
let parallelDrainError = null;
|
|
4006
|
+
const onFrameBuffer = async (frameIndex, buffer, workerSession) => {
|
|
4007
|
+
try {
|
|
4008
|
+
if (deParallelStream && workerSession.captureMode === "drawelement") {
|
|
4009
|
+
parallelGuardRan = true;
|
|
4010
|
+
buffer = await parallelGuard(workerSession, frameIndex, buffer);
|
|
4011
|
+
}
|
|
4012
|
+
await parallelReorder.waitForFrame(frameIndex);
|
|
4013
|
+
ensureFrameWritten(await currentEncoder.writeFrame(buffer), frameIndex, currentEncoder);
|
|
4014
|
+
parallelReorder.advanceTo(frameIndex + 1);
|
|
4015
|
+
writtenFrames = frameIndex + 1;
|
|
4016
|
+
} catch (err) {
|
|
4017
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
4018
|
+
if (!parallelDrainError) {
|
|
4019
|
+
parallelDrainError = e;
|
|
4020
|
+
parallelReorder.abort(e);
|
|
4021
|
+
}
|
|
4022
|
+
throw e;
|
|
3982
4023
|
}
|
|
3983
|
-
|
|
4024
|
+
};
|
|
4025
|
+
const captureLabel = input.parallelCaptureLabel ?? "worker";
|
|
4026
|
+
let watchdogArmed = false;
|
|
4027
|
+
const stallController = new AbortController();
|
|
4028
|
+
const forwardParentAbort = () => stallController.abort();
|
|
4029
|
+
if (abortSignal) {
|
|
4030
|
+
if (abortSignal.aborted) stallController.abort();
|
|
4031
|
+
else abortSignal.addEventListener("abort", forwardParentAbort, { once: true });
|
|
3984
4032
|
}
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
+
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
4034
|
+
let lastCapturedFrames = 0;
|
|
4035
|
+
let lastProgressAt = Date.now();
|
|
4036
|
+
const workerPhases = /* @__PURE__ */ new Map();
|
|
4037
|
+
const phaseDetail = (phase) => `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} step=${phase.readStep?.() ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
|
|
4038
|
+
const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
|
|
4039
|
+
let stalled = false;
|
|
4040
|
+
const stallTimer = setInterval(
|
|
4041
|
+
() => {
|
|
4042
|
+
if (!watchdogArmed || Date.now() - lastProgressAt <= stallTimeoutMs) return;
|
|
4043
|
+
stalled = true;
|
|
4044
|
+
const stallErr = new ParallelCaptureStallError({
|
|
4045
|
+
variant: "drain",
|
|
4046
|
+
captureLabel,
|
|
4047
|
+
stallTimeoutMs,
|
|
4048
|
+
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4049
|
+
totalFrames,
|
|
4050
|
+
phaseSummary: phaseSummary()
|
|
4051
|
+
});
|
|
4052
|
+
parallelReorder.abort(stallErr);
|
|
4053
|
+
stallController.abort();
|
|
4054
|
+
},
|
|
4055
|
+
Math.min(stallTimeoutMs, DE_STALL_POLL_MS)
|
|
4056
|
+
);
|
|
4057
|
+
try {
|
|
4058
|
+
workerResults = await executeParallelCapture(
|
|
4059
|
+
fileServer.url,
|
|
4060
|
+
workDir,
|
|
4061
|
+
tasks,
|
|
4062
|
+
buildCaptureOptions(),
|
|
4063
|
+
createRenderVideoFrameInjector,
|
|
4064
|
+
stallController.signal,
|
|
4065
|
+
(progress) => {
|
|
4066
|
+
if (progress.latestWorkerPhase) {
|
|
4067
|
+
const phase = progress.latestWorkerPhase;
|
|
4068
|
+
workerPhases.set(phase.workerId, phaseDetail(phase));
|
|
4069
|
+
if (phase.phase === "frame_capture" && !watchdogArmed) {
|
|
4070
|
+
watchdogArmed = true;
|
|
4071
|
+
lastProgressAt = Date.now();
|
|
4072
|
+
}
|
|
4073
|
+
log.info("[Render] Parallel capture worker phase", {
|
|
4074
|
+
workerId: phase.workerId,
|
|
4075
|
+
phase: phase.phase,
|
|
4076
|
+
frameIndex: phase.frameIndex,
|
|
4077
|
+
browserExecutable: phase.browserExecutable,
|
|
4078
|
+
browserVersion: phase.browserVersion,
|
|
4079
|
+
canvasDrawElement: phase.canvasDrawElement,
|
|
4080
|
+
gpuBackend: phase.gpuBackend
|
|
4081
|
+
});
|
|
4082
|
+
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
if (progress.capturedFrames > lastCapturedFrames) {
|
|
4086
|
+
lastCapturedFrames = progress.capturedFrames;
|
|
4033
4087
|
lastProgressAt = Date.now();
|
|
4088
|
+
watchdogArmed = true;
|
|
4089
|
+
}
|
|
4090
|
+
const doneFrames = resumeFrom + progress.capturedFrames;
|
|
4091
|
+
job.framesRendered = doneFrames;
|
|
4092
|
+
const frameProgress = doneFrames / totalFrames;
|
|
4093
|
+
reportFrameProgress(
|
|
4094
|
+
job,
|
|
4095
|
+
`Streaming frame ${doneFrames}/${totalFrames} (${workerCount} workers)`,
|
|
4096
|
+
Math.round(25 + frameProgress * 55),
|
|
4097
|
+
onProgress,
|
|
4098
|
+
doneFrames,
|
|
4099
|
+
totalFrames
|
|
4100
|
+
);
|
|
4101
|
+
},
|
|
4102
|
+
onFrameBuffer,
|
|
4103
|
+
// Interleaved DE workers each get their own browser process: co-tenant pages starve
|
|
4104
|
+
// each other's paint events.
|
|
4105
|
+
deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
|
|
4106
|
+
{
|
|
4107
|
+
// A worker died: release parked peers with its original failure, not the stall the
|
|
4108
|
+
// watchdog raises later.
|
|
4109
|
+
onWorkerOperation: (phase) => workerPhases.set(phase.workerId, phaseDetail(phase)),
|
|
4110
|
+
onWorkerFailure: (failure) => {
|
|
4111
|
+
if (parallelDrainError) return;
|
|
4112
|
+
parallelDrainError = failure;
|
|
4113
|
+
parallelReorder.abort(failure);
|
|
4034
4114
|
}
|
|
4035
|
-
log.info("[Render] Parallel capture worker phase", {
|
|
4036
|
-
workerId: phase.workerId,
|
|
4037
|
-
phase: phase.phase,
|
|
4038
|
-
frameIndex: phase.frameIndex,
|
|
4039
|
-
browserExecutable: phase.browserExecutable,
|
|
4040
|
-
browserVersion: phase.browserVersion,
|
|
4041
|
-
canvasDrawElement: phase.canvasDrawElement,
|
|
4042
|
-
gpuBackend: phase.gpuBackend
|
|
4043
|
-
});
|
|
4044
|
-
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
4045
|
-
return;
|
|
4046
|
-
}
|
|
4047
|
-
if (progress.capturedFrames > lastCapturedFrames) {
|
|
4048
|
-
lastCapturedFrames = progress.capturedFrames;
|
|
4049
|
-
lastProgressAt = Date.now();
|
|
4050
|
-
watchdogArmed = true;
|
|
4051
4115
|
}
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4116
|
+
);
|
|
4117
|
+
} catch (err) {
|
|
4118
|
+
if (stalled && abortSignal?.aborted !== true && captureLabel === "BeginFrame" && !resumedAfterStall && writtenFrames > resumeFrom) {
|
|
4119
|
+
resumedAfterStall = true;
|
|
4120
|
+
log.warn(
|
|
4121
|
+
"[Render] Parallel BeginFrame capture stalled; resuming from the first unwritten frame",
|
|
4122
|
+
{
|
|
4123
|
+
resumeFrom: writtenFrames,
|
|
4124
|
+
totalFrames,
|
|
4125
|
+
workerPhases: phaseSummary()
|
|
4126
|
+
}
|
|
4061
4127
|
);
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
// Interleaved DE workers each need their own browser PROCESS:
|
|
4065
|
-
// pages co-tenant in one browser share one compositor, whose
|
|
4066
|
-
// internal frame scheduling deprioritizes non-active pages — their
|
|
4067
|
-
// canvas `paint` events starve and the drawElement paint-wait slows
|
|
4068
|
-
// ~3x (measured: 86s vs 30s on a 3,245-frame rAF comp). This is
|
|
4069
|
-
// Chromium's internal frame-production scheduling on ALL platforms,
|
|
4070
|
-
// NOT the Linux-only HeadlessExperimental.beginFrame capture mode.
|
|
4071
|
-
deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
|
|
4072
|
-
{
|
|
4073
|
-
// A worker died (Target closed, Page crashed, launch failure).
|
|
4074
|
-
// Its frames are gone and peers are parked in waitForFrame on the
|
|
4075
|
-
// first of them; release them with the ORIGINAL failure so the
|
|
4076
|
-
// orchestrator sees a transient browser error it knows how to
|
|
4077
|
-
// retry, not the stall the watchdog would have raised a minute
|
|
4078
|
-
// later. First failure wins, same as the drain guard above.
|
|
4079
|
-
onWorkerFailure: (failure) => {
|
|
4080
|
-
if (parallelDrainError) return;
|
|
4081
|
-
parallelDrainError = failure;
|
|
4082
|
-
reorderBuffer.abort(failure);
|
|
4083
|
-
}
|
|
4128
|
+
resumeFrom = writtenFrames;
|
|
4129
|
+
continue;
|
|
4084
4130
|
}
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4131
|
+
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4132
|
+
if (stalled && abortSignal?.aborted !== true) {
|
|
4133
|
+
throw new ParallelCaptureStallError({
|
|
4134
|
+
variant: "pool",
|
|
4135
|
+
captureLabel,
|
|
4136
|
+
stallTimeoutMs,
|
|
4137
|
+
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4138
|
+
totalFrames,
|
|
4139
|
+
phaseSummary: phaseSummary()
|
|
4140
|
+
});
|
|
4141
|
+
}
|
|
4142
|
+
throw poolFailure(err, probeSession);
|
|
4143
|
+
} finally {
|
|
4144
|
+
clearInterval(stallTimer);
|
|
4145
|
+
abortSignal?.removeEventListener("abort", forwardParentAbort);
|
|
4097
4146
|
}
|
|
4098
|
-
throw poolFailure(
|
|
4099
|
-
|
|
4100
|
-
clearInterval(stallTimer);
|
|
4101
|
-
abortSignal?.removeEventListener("abort", forwardParentAbort);
|
|
4147
|
+
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4148
|
+
break;
|
|
4102
4149
|
}
|
|
4103
|
-
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4104
4150
|
pushWorkerDedupPerfs(workerResults, dedupPerfs);
|
|
4105
4151
|
const workerPageErrors = [];
|
|
4106
4152
|
pushWorkerPageErrors(workerResults, workerPageErrors);
|
|
@@ -4261,7 +4307,7 @@ function segmentEncoderOptions(base, segment) {
|
|
|
4261
4307
|
return {
|
|
4262
4308
|
...base,
|
|
4263
4309
|
lockGopForChunkConcat: true,
|
|
4264
|
-
gopSize: segment.endFrame - segment.startFrame
|
|
4310
|
+
gopSize: Math.min(base.gopSize ?? Infinity, segment.endFrame - segment.startFrame)
|
|
4265
4311
|
};
|
|
4266
4312
|
}
|
|
4267
4313
|
var SegmentEncoderSpawnError = class extends Error {
|
|
@@ -4355,6 +4401,12 @@ function defaultSessionFactory(input) {
|
|
|
4355
4401
|
let probeSession = input.probeSession;
|
|
4356
4402
|
return {
|
|
4357
4403
|
create: async () => {
|
|
4404
|
+
const probeCannotCaptureEveryFrame = Boolean(probeSession?.deRiskFrames?.size);
|
|
4405
|
+
if (probeSession && probeCannotCaptureEveryFrame) {
|
|
4406
|
+
await closeCaptureSession2(probeSession).catch(() => {
|
|
4407
|
+
});
|
|
4408
|
+
probeSession = null;
|
|
4409
|
+
}
|
|
4358
4410
|
const session = probeSession ?? await openSegmentedSession(input);
|
|
4359
4411
|
if (probeSession) {
|
|
4360
4412
|
openSegmentedSessionReuse(input, probeSession);
|
|
@@ -4685,8 +4737,10 @@ function computeSegmentPlanHash(input) {
|
|
|
4685
4737
|
input.motionBlur,
|
|
4686
4738
|
input.pageMotionBlur ?? "",
|
|
4687
4739
|
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
4688
|
-
]
|
|
4689
|
-
|
|
4740
|
+
];
|
|
4741
|
+
const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
|
|
4742
|
+
if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
|
|
4743
|
+
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
4690
4744
|
}
|
|
4691
4745
|
function segmentDirFor(projectRendersDir, planHash) {
|
|
4692
4746
|
return join11(projectRendersDir, ".hf-segments", planHash);
|
|
@@ -4833,7 +4887,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
|
4833
4887
|
|
|
4834
4888
|
// src/services/render/captureCost.ts
|
|
4835
4889
|
import { join as join12 } from "node:path";
|
|
4836
|
-
import { fpsToNumber as
|
|
4890
|
+
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
4837
4891
|
import {
|
|
4838
4892
|
calculateOptimalWorkers,
|
|
4839
4893
|
captureFrameToBuffer as captureFrameToBuffer3,
|
|
@@ -5037,7 +5091,7 @@ async function runCaptureCalibration(input) {
|
|
|
5037
5091
|
let probeSession = input.probeSession;
|
|
5038
5092
|
let forceScreenshot = input.forceScreenshot;
|
|
5039
5093
|
let lastBrowserConsole = [];
|
|
5040
|
-
const fps =
|
|
5094
|
+
const fps = fpsToNumber4(job.config.fps);
|
|
5041
5095
|
const sessionRef = { current: null };
|
|
5042
5096
|
const runOneCalibration = async (sessionDir, sessionCfg) => {
|
|
5043
5097
|
log.info("Launching browser for capture calibration...");
|
|
@@ -5497,7 +5551,7 @@ function emitFallbackCaptureProfile(recorder, perfSummaries, env = process.env)
|
|
|
5497
5551
|
// src/services/render/videoFrameCoverage.ts
|
|
5498
5552
|
import { parseHTML } from "linkedom";
|
|
5499
5553
|
import {
|
|
5500
|
-
fpsToNumber as
|
|
5554
|
+
fpsToNumber as fpsToNumber5,
|
|
5501
5555
|
normalizeRateSpec,
|
|
5502
5556
|
sourceTimeAt,
|
|
5503
5557
|
toFps
|
|
@@ -5532,7 +5586,7 @@ function resolveVideoCoverageThreshold(envValue = process.env.HF_VIDEO_COVERAGE_
|
|
|
5532
5586
|
return parsed;
|
|
5533
5587
|
}
|
|
5534
5588
|
function expectedFramesForClip(start, end, fps, rounding = "ceil") {
|
|
5535
|
-
const fpsValue =
|
|
5589
|
+
const fpsValue = fpsToNumber5(toFps(fps));
|
|
5536
5590
|
if (!Number.isFinite(start) || !Number.isFinite(end) || !Number.isFinite(fpsValue)) return 0;
|
|
5537
5591
|
if (fpsValue <= 0) return 0;
|
|
5538
5592
|
const duration = Math.max(0, end - start);
|
|
@@ -6794,7 +6848,7 @@ function detectShaderTransitionUsage(html) {
|
|
|
6794
6848
|
}
|
|
6795
6849
|
return false;
|
|
6796
6850
|
}
|
|
6797
|
-
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
|
|
6851
|
+
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
|
|
6798
6852
|
let filePath = src;
|
|
6799
6853
|
if (isHttpUrl(src)) {
|
|
6800
6854
|
if (!existsSync10(downloadDir)) mkdirSync7(downloadDir, { recursive: true });
|
|
@@ -6821,45 +6875,52 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6821
6875
|
if (error instanceof Error && error.stack) wrapped.stack = error.stack;
|
|
6822
6876
|
return wrapped;
|
|
6823
6877
|
};
|
|
6824
|
-
|
|
6825
|
-
let profile;
|
|
6878
|
+
const probe = () => withMediaProbeSlot(async () => {
|
|
6826
6879
|
try {
|
|
6827
6880
|
await assertMediaPayload(filePath, elementIdentity);
|
|
6828
|
-
profile = await probeMediaProfile2(filePath);
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
if (error instanceof NotMediaPayloadError2) {
|
|
6832
|
-
log?.warn(
|
|
6833
|
-
`[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
|
|
6834
|
-
);
|
|
6835
|
-
}
|
|
6836
|
-
return { duration: null, resolvedPath: filePath };
|
|
6837
|
-
}
|
|
6838
|
-
throw withSrcContext(error);
|
|
6839
|
-
}
|
|
6840
|
-
assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);
|
|
6841
|
-
let metadata;
|
|
6842
|
-
if (tagName === "video") {
|
|
6843
|
-
try {
|
|
6844
|
-
metadata = await extractMediaMetadata(filePath);
|
|
6845
|
-
} catch (error) {
|
|
6846
|
-
throw withSrcContext(error);
|
|
6881
|
+
const profile = await probeMediaProfile2(filePath);
|
|
6882
|
+
if (tagName === "video") {
|
|
6883
|
+
return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
|
|
6847
6884
|
}
|
|
6848
|
-
} else {
|
|
6849
6885
|
try {
|
|
6850
|
-
|
|
6886
|
+
return { profile, fileDuration: (await extractAudioMetadata(filePath)).durationSeconds };
|
|
6851
6887
|
} catch {
|
|
6852
|
-
return {
|
|
6888
|
+
return { profile, fileDuration: null };
|
|
6853
6889
|
}
|
|
6890
|
+
} catch (error) {
|
|
6891
|
+
return { error };
|
|
6854
6892
|
}
|
|
6855
|
-
const fileDuration = metadata.durationSeconds;
|
|
6856
|
-
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
6857
|
-
fileDuration,
|
|
6858
|
-
mediaStart,
|
|
6859
|
-
playbackRate
|
|
6860
|
-
);
|
|
6861
|
-
return { duration, resolvedPath: filePath };
|
|
6862
6893
|
});
|
|
6894
|
+
const key = `${tagName}\0${filePath}`;
|
|
6895
|
+
let probed = probes?.get(key);
|
|
6896
|
+
if (!probed) {
|
|
6897
|
+
probed = probe();
|
|
6898
|
+
probes?.set(key, probed);
|
|
6899
|
+
}
|
|
6900
|
+
const outcome = await probed;
|
|
6901
|
+
if ("error" in outcome) {
|
|
6902
|
+
if (tagName !== "video") {
|
|
6903
|
+
if (outcome.error instanceof NotMediaPayloadError2) {
|
|
6904
|
+
log?.warn(
|
|
6905
|
+
`[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
|
|
6906
|
+
);
|
|
6907
|
+
}
|
|
6908
|
+
return { duration: null, resolvedPath: filePath };
|
|
6909
|
+
}
|
|
6910
|
+
throw withSrcContext(outcome.error);
|
|
6911
|
+
}
|
|
6912
|
+
assertAssetMediaTypeProfile(
|
|
6913
|
+
tagName === "video" ? "video" : "audio",
|
|
6914
|
+
outcome.profile,
|
|
6915
|
+
elementIdentity
|
|
6916
|
+
);
|
|
6917
|
+
if (outcome.fileDuration === null) return { duration: null, resolvedPath: filePath };
|
|
6918
|
+
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
6919
|
+
outcome.fileDuration,
|
|
6920
|
+
mediaStart,
|
|
6921
|
+
playbackRate
|
|
6922
|
+
);
|
|
6923
|
+
return { duration, resolvedPath: filePath };
|
|
6863
6924
|
}
|
|
6864
6925
|
function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
6865
6926
|
const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
|
|
@@ -6873,7 +6934,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
|
6873
6934
|
}
|
|
6874
6935
|
return changed ? document2.toString() : html;
|
|
6875
6936
|
}
|
|
6876
|
-
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
|
|
6937
|
+
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath, probes) {
|
|
6877
6938
|
const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
|
|
6878
6939
|
const mediaUnresolved = unresolved.filter(
|
|
6879
6940
|
(el) => (el.tagName === "video" || el.tagName === "audio") && el.src
|
|
@@ -6890,7 +6951,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6890
6951
|
el.tagName,
|
|
6891
6952
|
el.id,
|
|
6892
6953
|
log,
|
|
6893
|
-
urlByAbsentLocalPath
|
|
6954
|
+
urlByAbsentLocalPath,
|
|
6955
|
+
probes
|
|
6894
6956
|
).then(({ duration }) => ({ id: el.id, duration }))
|
|
6895
6957
|
)
|
|
6896
6958
|
);
|
|
@@ -6911,7 +6973,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6911
6973
|
downloadDir,
|
|
6912
6974
|
el.tagName,
|
|
6913
6975
|
el.id,
|
|
6914
|
-
log
|
|
6976
|
+
log,
|
|
6977
|
+
void 0,
|
|
6978
|
+
probes
|
|
6915
6979
|
);
|
|
6916
6980
|
return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
|
|
6917
6981
|
})
|
|
@@ -6933,7 +6997,7 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6933
6997
|
compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
|
|
6934
6998
|
return { html: compiledHtml, unresolvedCompositions };
|
|
6935
6999
|
}
|
|
6936
|
-
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
|
|
7000
|
+
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath, probes) {
|
|
6937
7001
|
const subCompositions = /* @__PURE__ */ new Map();
|
|
6938
7002
|
const { document: document2 } = parseHTML5(html);
|
|
6939
7003
|
const compEls = document2.querySelectorAll("[data-composition-src]");
|
|
@@ -6961,14 +7025,16 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
6961
7025
|
dirname7(item.filePath),
|
|
6962
7026
|
downloadDir,
|
|
6963
7027
|
void 0,
|
|
6964
|
-
urlByAbsentLocalPath
|
|
7028
|
+
urlByAbsentLocalPath,
|
|
7029
|
+
probes
|
|
6965
7030
|
);
|
|
6966
7031
|
const nested = await parseSubCompositions(
|
|
6967
7032
|
compiledSub,
|
|
6968
7033
|
projectDir,
|
|
6969
7034
|
downloadDir,
|
|
6970
7035
|
item.nestedVisited,
|
|
6971
|
-
urlByAbsentLocalPath
|
|
7036
|
+
urlByAbsentLocalPath,
|
|
7037
|
+
probes
|
|
6972
7038
|
);
|
|
6973
7039
|
return {
|
|
6974
7040
|
srcPath: item.srcPath,
|
|
@@ -7738,19 +7804,22 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7738
7804
|
);
|
|
7739
7805
|
const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
|
|
7740
7806
|
assertSubCompositionsUsable(rawHtml, projectDir);
|
|
7807
|
+
const probes = /* @__PURE__ */ new Map();
|
|
7741
7808
|
const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
|
|
7742
7809
|
rawHtml,
|
|
7743
7810
|
projectDir,
|
|
7744
7811
|
downloadDir,
|
|
7745
7812
|
options.log,
|
|
7746
|
-
options.urlByAbsentLocalPath
|
|
7813
|
+
options.urlByAbsentLocalPath,
|
|
7814
|
+
probes
|
|
7747
7815
|
);
|
|
7748
7816
|
const { subCompositions } = await parseSubCompositions(
|
|
7749
7817
|
compiledHtml,
|
|
7750
7818
|
projectDir,
|
|
7751
7819
|
downloadDir,
|
|
7752
7820
|
void 0,
|
|
7753
|
-
options.urlByAbsentLocalPath
|
|
7821
|
+
options.urlByAbsentLocalPath,
|
|
7822
|
+
probes
|
|
7754
7823
|
);
|
|
7755
7824
|
const fullHtml = ensureFullDocument(compiledHtml);
|
|
7756
7825
|
const inlinedHtml = inlineSubCompositions(
|
|
@@ -7853,9 +7922,12 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7853
7922
|
}
|
|
7854
7923
|
const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
|
|
7855
7924
|
const masterFxChain = parseMasterFxChain(html);
|
|
7925
|
+
const advised = /* @__PURE__ */ new Set();
|
|
7856
7926
|
for (const video of videos) {
|
|
7857
7927
|
if (isHttpUrl(video.src)) continue;
|
|
7858
7928
|
const videoPath = resolve7(projectDir, video.src);
|
|
7929
|
+
if (advised.has(videoPath)) continue;
|
|
7930
|
+
advised.add(videoPath);
|
|
7859
7931
|
const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
|
|
7860
7932
|
(async () => {
|
|
7861
7933
|
const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
|
|
@@ -8406,13 +8478,14 @@ import {
|
|
|
8406
8478
|
} from "@hyperframes/engine";
|
|
8407
8479
|
import {
|
|
8408
8480
|
MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
|
|
8409
|
-
fpsToNumber as
|
|
8481
|
+
fpsToNumber as fpsToNumber6,
|
|
8410
8482
|
normalizeRateSpec as normalizeRateSpec3,
|
|
8411
8483
|
readMediaStart as readMediaStart2,
|
|
8412
8484
|
shiftRateLane as shiftRateLane2,
|
|
8413
8485
|
sourceTimeAt as sourceTimeAt3,
|
|
8414
8486
|
timeAtSourceTime
|
|
8415
8487
|
} from "@hyperframes/core";
|
|
8488
|
+
import { parseHostVariableValues as parseHostVariableValues2, readDeclaredDefaults as readDeclaredDefaults2 } from "@hyperframes/core/compiler";
|
|
8416
8489
|
import { extractMediaSrcMutations } from "@hyperframes/parsers";
|
|
8417
8490
|
|
|
8418
8491
|
// src/services/render/stages/probeFailures.ts
|
|
@@ -8450,8 +8523,15 @@ function hasAutoStartVideos(html) {
|
|
|
8450
8523
|
const { document: document2 } = parseHTML6(html);
|
|
8451
8524
|
return document2.querySelector("video[data-hf-auto-start]") !== null;
|
|
8452
8525
|
}
|
|
8526
|
+
function variableHasValue(element, variableId, variables) {
|
|
8527
|
+
if (variables && Object.hasOwn(variables, variableId)) return true;
|
|
8528
|
+
for (let scope = element.parentElement; scope; scope = scope.parentElement) {
|
|
8529
|
+
if (Object.hasOwn(parseHostVariableValues2(scope), variableId)) return true;
|
|
8530
|
+
if (Object.hasOwn(readDeclaredDefaults2(scope), variableId)) return true;
|
|
8531
|
+
}
|
|
8532
|
+
return Object.hasOwn(readDeclaredDefaults2(element.ownerDocument.documentElement), variableId);
|
|
8533
|
+
}
|
|
8453
8534
|
function hasVariableBoundMedia(html, variables) {
|
|
8454
|
-
if (!variables || Object.keys(variables).length === 0) return false;
|
|
8455
8535
|
const { document: document2 } = parseHTML6(html);
|
|
8456
8536
|
return Array.from(
|
|
8457
8537
|
document2.querySelectorAll(
|
|
@@ -8459,7 +8539,7 @@ function hasVariableBoundMedia(html, variables) {
|
|
|
8459
8539
|
)
|
|
8460
8540
|
).some((element) => {
|
|
8461
8541
|
const variableId = element.getAttribute("data-var-src")?.trim();
|
|
8462
|
-
return Boolean(variableId &&
|
|
8542
|
+
return Boolean(variableId && variableHasValue(element, variableId, variables));
|
|
8463
8543
|
});
|
|
8464
8544
|
}
|
|
8465
8545
|
function authoredMediaStarts(html) {
|
|
@@ -8885,7 +8965,7 @@ async function runProbeStage(input) {
|
|
|
8885
8965
|
session.page,
|
|
8886
8966
|
composition.audios.map((audio) => audio.id),
|
|
8887
8967
|
composition.duration,
|
|
8888
|
-
|
|
8968
|
+
fpsToNumber6(job.config.fps)
|
|
8889
8969
|
);
|
|
8890
8970
|
assertNotAborted();
|
|
8891
8971
|
if (automation.length > 0) {
|
|
@@ -8948,7 +9028,7 @@ async function runProbeStage(input) {
|
|
|
8948
9028
|
}
|
|
8949
9029
|
const browserProbeMs = Date.now() - probeStart;
|
|
8950
9030
|
const duration = composition.duration;
|
|
8951
|
-
const totalFrames = durationToFrameCount(duration,
|
|
9031
|
+
const totalFrames = durationToFrameCount(duration, fpsToNumber6(job.config.fps));
|
|
8952
9032
|
if (duration <= 0) {
|
|
8953
9033
|
const diagnostics = [];
|
|
8954
9034
|
try {
|
|
@@ -9448,16 +9528,17 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
9448
9528
|
|
|
9449
9529
|
// src/services/render/stages/audioStage.ts
|
|
9450
9530
|
import { join as join20 } from "node:path";
|
|
9451
|
-
import { rmSync as rmSync10 } from "node:fs";
|
|
9452
9531
|
import {
|
|
9453
9532
|
MIXED_AUDIO_FILENAME,
|
|
9454
9533
|
audioFailureDiagnostics,
|
|
9455
|
-
processCompositionAudio
|
|
9534
|
+
processCompositionAudio,
|
|
9535
|
+
removeScratch as removeScratch2,
|
|
9536
|
+
collectScratchLeftovers
|
|
9456
9537
|
} from "@hyperframes/engine";
|
|
9457
9538
|
|
|
9458
9539
|
// src/services/render/audioPadTrim.ts
|
|
9459
9540
|
import { spawn } from "node:child_process";
|
|
9460
|
-
import { mkdtempSync as mkdtempSync3
|
|
9541
|
+
import { mkdtempSync as mkdtempSync3 } from "node:fs";
|
|
9461
9542
|
import { dirname as dirname8, join as join19 } from "node:path";
|
|
9462
9543
|
import {
|
|
9463
9544
|
DEFAULT_CONFIG,
|
|
@@ -9466,6 +9547,7 @@ import {
|
|
|
9466
9547
|
extractAudioMetadata as extractAudioMetadata3,
|
|
9467
9548
|
getFfprobeBinary,
|
|
9468
9549
|
ManagedChildProcess,
|
|
9550
|
+
removeScratch,
|
|
9469
9551
|
renameWithRetry as renameWithRetry5,
|
|
9470
9552
|
runFfmpeg as runFfmpeg5,
|
|
9471
9553
|
trackChildProcess
|
|
@@ -9661,7 +9743,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
9661
9743
|
error: `audioPadTrim: failed to materialize ${plan.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
9662
9744
|
};
|
|
9663
9745
|
} finally {
|
|
9664
|
-
for (const path of plan.cleanupPaths)
|
|
9746
|
+
for (const path of plan.cleanupPaths) removeScratch(path);
|
|
9665
9747
|
}
|
|
9666
9748
|
let loweredDb = 0;
|
|
9667
9749
|
if (probeTruePeak) {
|
|
@@ -9724,7 +9806,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9724
9806
|
}
|
|
9725
9807
|
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
9726
9808
|
if (measuredPath !== correctedPath) return { success: true };
|
|
9727
|
-
renameWithRetry5(correctedPath, input.audioPath);
|
|
9809
|
+
renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
|
|
9728
9810
|
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
9729
9811
|
}
|
|
9730
9812
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
@@ -9765,7 +9847,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9765
9847
|
)}`
|
|
9766
9848
|
};
|
|
9767
9849
|
} finally {
|
|
9768
|
-
if (scratchDir)
|
|
9850
|
+
if (scratchDir) removeScratch(scratchDir);
|
|
9769
9851
|
}
|
|
9770
9852
|
}
|
|
9771
9853
|
function failResult(outputPath, target, source, error) {
|
|
@@ -9910,7 +9992,15 @@ function assertAudioNotInterrupted(failures) {
|
|
|
9910
9992
|
])
|
|
9911
9993
|
});
|
|
9912
9994
|
}
|
|
9913
|
-
|
|
9995
|
+
function runAudioStage(input) {
|
|
9996
|
+
return collectScratchLeftovers(
|
|
9997
|
+
() => runAudioStageScoped(input),
|
|
9998
|
+
(left) => {
|
|
9999
|
+
if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
|
|
10000
|
+
}
|
|
10001
|
+
);
|
|
10002
|
+
}
|
|
10003
|
+
async function runAudioStageScoped(input) {
|
|
9914
10004
|
const stageStart = Date.now();
|
|
9915
10005
|
const pcmSourcePath = join20(input.workDir, "audio-source.wav");
|
|
9916
10006
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
@@ -9932,7 +10022,7 @@ async function runAudioStage(input) {
|
|
|
9932
10022
|
}
|
|
9933
10023
|
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
9934
10024
|
} finally {
|
|
9935
|
-
|
|
10025
|
+
removeScratch2(pcmSourcePath);
|
|
9936
10026
|
}
|
|
9937
10027
|
}
|
|
9938
10028
|
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
@@ -10412,7 +10502,7 @@ import {
|
|
|
10412
10502
|
initializeSession as initializeSession7,
|
|
10413
10503
|
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
10414
10504
|
} from "@hyperframes/engine";
|
|
10415
|
-
import { fpsToNumber as
|
|
10505
|
+
import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
|
|
10416
10506
|
|
|
10417
10507
|
// src/services/hdrImageTransferCache.ts
|
|
10418
10508
|
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
@@ -11086,6 +11176,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11086
11176
|
preset,
|
|
11087
11177
|
effectiveQuality,
|
|
11088
11178
|
effectiveBitrate,
|
|
11179
|
+
deliveryEncoder,
|
|
11089
11180
|
fileServer,
|
|
11090
11181
|
buildCaptureOptions,
|
|
11091
11182
|
createRenderVideoFrameInjector,
|
|
@@ -11147,7 +11238,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11147
11238
|
}
|
|
11148
11239
|
return map;
|
|
11149
11240
|
});
|
|
11150
|
-
const fpsDecimal =
|
|
11241
|
+
const fpsDecimal = fpsToNumber7(job.config.fps);
|
|
11151
11242
|
const transitionRanges = transitionMeta.map((t) => ({
|
|
11152
11243
|
...t,
|
|
11153
11244
|
startFrame: Math.floor(t.time * fpsDecimal),
|
|
@@ -11175,6 +11266,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11175
11266
|
preset: preset.preset,
|
|
11176
11267
|
quality: effectiveQuality,
|
|
11177
11268
|
bitrate: effectiveBitrate,
|
|
11269
|
+
...deliveryEncoder,
|
|
11178
11270
|
pixelFormat: preset.pixelFormat,
|
|
11179
11271
|
hdr: preset.hdr,
|
|
11180
11272
|
rawInputFormat: "rgb48le"
|
|
@@ -11234,7 +11326,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11234
11326
|
beforeCaptureHook: domSession.onBeforeCapture,
|
|
11235
11327
|
width,
|
|
11236
11328
|
height,
|
|
11237
|
-
fps:
|
|
11329
|
+
fps: fpsToNumber7(job.config.fps),
|
|
11238
11330
|
compositeTransfer,
|
|
11239
11331
|
nativeHdrImageIds,
|
|
11240
11332
|
hdrImageBuffers,
|
|
@@ -11378,7 +11470,7 @@ import {
|
|
|
11378
11470
|
mkdirSync as mkdirSync9,
|
|
11379
11471
|
readdirSync as readdirSync4,
|
|
11380
11472
|
readFileSync as readFileSync8,
|
|
11381
|
-
rmSync as
|
|
11473
|
+
rmSync as rmSync9,
|
|
11382
11474
|
statSync as statSync7,
|
|
11383
11475
|
writeFileSync as writeFileSync5
|
|
11384
11476
|
} from "node:fs";
|
|
@@ -11517,7 +11609,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
11517
11609
|
fileSize
|
|
11518
11610
|
};
|
|
11519
11611
|
} finally {
|
|
11520
|
-
|
|
11612
|
+
rmSync9(input.palettePath, { force: true });
|
|
11521
11613
|
}
|
|
11522
11614
|
}
|
|
11523
11615
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -11541,6 +11633,8 @@ function videoEncoderOptions(input, engineCfg) {
|
|
|
11541
11633
|
preset: preset.preset,
|
|
11542
11634
|
quality: input.effectiveQuality,
|
|
11543
11635
|
bitrate: input.effectiveBitrate,
|
|
11636
|
+
maxBitrate: input.maxBitrate,
|
|
11637
|
+
bufferSize: input.bufferSize,
|
|
11544
11638
|
pixelFormat: preset.pixelFormat,
|
|
11545
11639
|
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
11546
11640
|
useGpu: job.config.useGpu,
|
|
@@ -12815,6 +12909,7 @@ function resolveRenderDebugDir() {
|
|
|
12815
12909
|
}
|
|
12816
12910
|
async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
|
|
12817
12911
|
validateHlsRenderConfig(job.config);
|
|
12912
|
+
validateDeliveryEncoderConfig(job.config);
|
|
12818
12913
|
const debugDir = resolveRenderDebugDir();
|
|
12819
12914
|
const outputDir = dirname11(outputPath);
|
|
12820
12915
|
if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
|
|
@@ -12835,7 +12930,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
12835
12930
|
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
12836
12931
|
return;
|
|
12837
12932
|
}
|
|
12838
|
-
|
|
12933
|
+
rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
12839
12934
|
});
|
|
12840
12935
|
try {
|
|
12841
12936
|
await executeRenderPipeline({
|
|
@@ -12893,11 +12988,14 @@ async function executeRenderPipeline(input) {
|
|
|
12893
12988
|
const isGif = outputFormat === "gif";
|
|
12894
12989
|
const isHls = outputFormat === "hls";
|
|
12895
12990
|
const hlsSegmentSeconds = isHls ? resolveHlsSegmentSeconds(job.config) : void 0;
|
|
12896
|
-
const
|
|
12897
|
-
outputFormat,
|
|
12898
|
-
job.config.fps,
|
|
12899
|
-
job.config.hlsSegmentSeconds
|
|
12900
|
-
|
|
12991
|
+
const deliveryEncoder = resolveDeliveryEncoderOptions({
|
|
12992
|
+
format: outputFormat,
|
|
12993
|
+
fps: job.config.fps,
|
|
12994
|
+
hlsSegmentSeconds: job.config.hlsSegmentSeconds,
|
|
12995
|
+
keyframeIntervalSeconds: job.config.keyframeIntervalSeconds,
|
|
12996
|
+
maxBitrate: job.config.maxBitrate,
|
|
12997
|
+
bufferSize: job.config.bufferSize
|
|
12998
|
+
});
|
|
12901
12999
|
const artifactTransaction = new ArtifactTransaction(
|
|
12902
13000
|
outputPath,
|
|
12903
13001
|
isPngSequence || isHls ? "directory" : "file",
|
|
@@ -13137,7 +13235,7 @@ async function executeRenderPipeline(input) {
|
|
|
13137
13235
|
validateRenderDuration({
|
|
13138
13236
|
duration: probeResult.duration,
|
|
13139
13237
|
totalFrames,
|
|
13140
|
-
fps:
|
|
13238
|
+
fps: fpsToNumber8(job.config.fps)
|
|
13141
13239
|
});
|
|
13142
13240
|
perfStages.browserProbeMs = probeResult.browserProbeMs;
|
|
13143
13241
|
perfStages.compileMs = Date.now() - stage1Start;
|
|
@@ -13980,6 +14078,7 @@ async function executeRenderPipeline(input) {
|
|
|
13980
14078
|
preset,
|
|
13981
14079
|
effectiveQuality,
|
|
13982
14080
|
effectiveBitrate,
|
|
14081
|
+
deliveryEncoder,
|
|
13983
14082
|
fileServer: activeFileServer,
|
|
13984
14083
|
buildCaptureOptions,
|
|
13985
14084
|
createRenderVideoFrameInjector,
|
|
@@ -14004,9 +14103,7 @@ async function executeRenderPipeline(input) {
|
|
|
14004
14103
|
} else {
|
|
14005
14104
|
let streamingHandled = false;
|
|
14006
14105
|
if (capturePlan.kind === "sdr_segmented") {
|
|
14007
|
-
const segmentedPlan = capturePlan;
|
|
14008
14106
|
const captureFrameStart = Date.now();
|
|
14009
|
-
resetCaptureAttemptProgress(job);
|
|
14010
14107
|
const segmentFrames = resolveSegmentFrames(process.env);
|
|
14011
14108
|
const segmentImageFormat = captureOptions.format || "jpeg";
|
|
14012
14109
|
const segmentPlanHash = computeSegmentPlanHash({
|
|
@@ -14021,6 +14118,9 @@ async function executeRenderPipeline(input) {
|
|
|
14021
14118
|
preset: preset.preset,
|
|
14022
14119
|
quality: effectiveQuality,
|
|
14023
14120
|
bitrate: effectiveBitrate,
|
|
14121
|
+
maxBitrate: deliveryEncoder.maxBitrate,
|
|
14122
|
+
bufferSize: deliveryEncoder.bufferSize,
|
|
14123
|
+
gopSize: deliveryEncoder.gopSize,
|
|
14024
14124
|
pixelFormat: preset.pixelFormat,
|
|
14025
14125
|
imageFormat: segmentImageFormat,
|
|
14026
14126
|
useGpu: job.config.useGpu === true,
|
|
@@ -14046,71 +14146,129 @@ async function executeRenderPipeline(input) {
|
|
|
14046
14146
|
});
|
|
14047
14147
|
}
|
|
14048
14148
|
if (completedSegments.size === 0) {
|
|
14049
|
-
|
|
14149
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14050
14150
|
}
|
|
14051
14151
|
} else {
|
|
14052
|
-
|
|
14152
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14053
14153
|
}
|
|
14054
|
-
const
|
|
14154
|
+
const emptySegmentManifest = () => ({
|
|
14055
14155
|
version: 1,
|
|
14056
14156
|
planHash: segmentPlanHash,
|
|
14057
14157
|
totalFrames,
|
|
14058
14158
|
segmentFrames,
|
|
14059
14159
|
completed: []
|
|
14160
|
+
});
|
|
14161
|
+
let segmentManifest = readSegmentManifest(segmentDir) ?? emptySegmentManifest();
|
|
14162
|
+
const invokeSegmented = () => {
|
|
14163
|
+
if (capturePlan.kind !== "sdr_segmented") {
|
|
14164
|
+
throw new Error(`Cannot invoke segmented stage with ${capturePlan.kind} plan`);
|
|
14165
|
+
}
|
|
14166
|
+
const segmentedPlan = capturePlan;
|
|
14167
|
+
resetCaptureAttemptProgress(job);
|
|
14168
|
+
return observeRenderStage(
|
|
14169
|
+
observability,
|
|
14170
|
+
"capture_segmented",
|
|
14171
|
+
captureStageObservationData(),
|
|
14172
|
+
() => runCaptureSegmentedStage({
|
|
14173
|
+
fileServer: activeFileServer,
|
|
14174
|
+
workDir,
|
|
14175
|
+
framesDir,
|
|
14176
|
+
videoOnlyPath,
|
|
14177
|
+
job,
|
|
14178
|
+
totalFrames,
|
|
14179
|
+
cfg,
|
|
14180
|
+
plan: segmentedPlan,
|
|
14181
|
+
log,
|
|
14182
|
+
probeSession,
|
|
14183
|
+
outputFormat,
|
|
14184
|
+
streamingEncoderOptions: {
|
|
14185
|
+
fps: job.config.fps,
|
|
14186
|
+
width: captureCompositionWidth ?? width,
|
|
14187
|
+
height: captureCompositionHeight ?? height,
|
|
14188
|
+
codec: preset.codec,
|
|
14189
|
+
preset: preset.preset,
|
|
14190
|
+
quality: effectiveQuality,
|
|
14191
|
+
bitrate: effectiveBitrate,
|
|
14192
|
+
pixelFormat: preset.pixelFormat,
|
|
14193
|
+
vp9CpuUsed: cfg.vp9CpuUsed,
|
|
14194
|
+
useGpu: job.config.useGpu,
|
|
14195
|
+
imageFormat: segmentImageFormat,
|
|
14196
|
+
hdr: preset.hdr,
|
|
14197
|
+
// Each segment sets its own GOP (at most the interval); hls never reaches this route.
|
|
14198
|
+
...deliveryEncoder
|
|
14199
|
+
},
|
|
14200
|
+
buildCaptureOptions,
|
|
14201
|
+
createRenderVideoFrameInjector,
|
|
14202
|
+
abortSignal: executionSignal,
|
|
14203
|
+
assertNotAborted,
|
|
14204
|
+
onProgress,
|
|
14205
|
+
dedupPerfs,
|
|
14206
|
+
segmentFrames,
|
|
14207
|
+
segmentDir,
|
|
14208
|
+
workerCount: segmentedPlan.workerCount,
|
|
14209
|
+
browserRecycleEverySegments: resolveSegmentBrowserRecycle(process.env),
|
|
14210
|
+
completedSegments,
|
|
14211
|
+
onSegmentComplete: (entry) => {
|
|
14212
|
+
segmentManifest.completed = [
|
|
14213
|
+
...segmentManifest.completed.filter((e) => e.index !== entry.index),
|
|
14214
|
+
{ ...entry, completedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
14215
|
+
];
|
|
14216
|
+
writeSegmentManifest(segmentDir, segmentManifest);
|
|
14217
|
+
},
|
|
14218
|
+
updateCaptureObservability
|
|
14219
|
+
})
|
|
14220
|
+
);
|
|
14060
14221
|
};
|
|
14061
|
-
|
|
14062
|
-
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14097
|
-
|
|
14098
|
-
|
|
14099
|
-
|
|
14100
|
-
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
14104
|
-
|
|
14105
|
-
|
|
14106
|
-
|
|
14107
|
-
|
|
14108
|
-
|
|
14109
|
-
|
|
14110
|
-
|
|
14111
|
-
updateCaptureObservability
|
|
14112
|
-
})
|
|
14113
|
-
);
|
|
14222
|
+
let segmentedRes;
|
|
14223
|
+
try {
|
|
14224
|
+
segmentedRes = await invokeSegmented();
|
|
14225
|
+
} catch (err) {
|
|
14226
|
+
const isVerifyError = isDrawElementVerificationError(err);
|
|
14227
|
+
const isDeStall = isDeRendererStallError(err);
|
|
14228
|
+
if (!isVerifyError && !isDrawElementCaptureError(err) && !isDeStall || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
|
|
14229
|
+
throw err;
|
|
14230
|
+
}
|
|
14231
|
+
deSelfVerifyFallback = isVerifyError;
|
|
14232
|
+
const t = deVerifyFallbackTelemetry(err);
|
|
14233
|
+
deFallbackReason = isVerifyError ? t.reason : isDeStall ? "de_renderer_stall" : "capture_error";
|
|
14234
|
+
deFallbackFailedDb = t.failedDb;
|
|
14235
|
+
deFallbackFrameIndex = t.frameIndex;
|
|
14236
|
+
deFallbackThresholdDb = t.thresholdDb;
|
|
14237
|
+
log.warn(
|
|
14238
|
+
"[Render] drawElement capture failed on the segmented path; re-rendering via screenshot",
|
|
14239
|
+
{
|
|
14240
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14241
|
+
}
|
|
14242
|
+
);
|
|
14243
|
+
observability.checkpoint(
|
|
14244
|
+
"capture_segmented",
|
|
14245
|
+
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
14246
|
+
);
|
|
14247
|
+
if (probeSession) {
|
|
14248
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
14249
|
+
const orphaned = probeSession;
|
|
14250
|
+
probeSession = null;
|
|
14251
|
+
await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
|
|
14252
|
+
}
|
|
14253
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14254
|
+
completedSegments = /* @__PURE__ */ new Set();
|
|
14255
|
+
segmentManifest = emptySegmentManifest();
|
|
14256
|
+
dedupPerfs.length = 0;
|
|
14257
|
+
capturePlan = replanAfterFailure(capturePlan, {
|
|
14258
|
+
kind: isVerifyError ? "draw_element_verification" : "draw_element_capture"
|
|
14259
|
+
});
|
|
14260
|
+
syncCapturePlan();
|
|
14261
|
+
updateCaptureObservability({
|
|
14262
|
+
forceScreenshot: capturePlan.forceScreenshot,
|
|
14263
|
+
deSelfVerifyFallback,
|
|
14264
|
+
deFallbackReason,
|
|
14265
|
+
deFallbackFailedDb,
|
|
14266
|
+
deFallbackFrameIndex,
|
|
14267
|
+
deFallbackThresholdDb
|
|
14268
|
+
});
|
|
14269
|
+
segmentedRes = await invokeSegmented();
|
|
14270
|
+
observability.clearFailure("capture_segmented");
|
|
14271
|
+
}
|
|
14114
14272
|
if (segmentedRes.success) {
|
|
14115
14273
|
streamingHandled = true;
|
|
14116
14274
|
workerCount = segmentedRes.workerCount;
|
|
@@ -14130,7 +14288,7 @@ async function executeRenderPipeline(input) {
|
|
|
14130
14288
|
);
|
|
14131
14289
|
updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
|
|
14132
14290
|
if (job.config.keepSegments !== true) {
|
|
14133
|
-
|
|
14291
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14134
14292
|
}
|
|
14135
14293
|
} else {
|
|
14136
14294
|
capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
|
|
@@ -14183,7 +14341,7 @@ async function executeRenderPipeline(input) {
|
|
|
14183
14341
|
// assemble stage's `-c copy` segmentation cuts exactly on
|
|
14184
14342
|
// the segment boundary. Every other format leaves the
|
|
14185
14343
|
// encoder's open-GOP output untouched.
|
|
14186
|
-
...
|
|
14344
|
+
...deliveryEncoder
|
|
14187
14345
|
},
|
|
14188
14346
|
buildCaptureOptions,
|
|
14189
14347
|
createRenderVideoFrameInjector,
|
|
@@ -14346,7 +14504,7 @@ async function executeRenderPipeline(input) {
|
|
|
14346
14504
|
engineConfig: cfg,
|
|
14347
14505
|
// Same value the streaming encoder above spreads — the disk and
|
|
14348
14506
|
// chunked-concat encoders need an identical GOP lock for HLS.
|
|
14349
|
-
...
|
|
14507
|
+
...deliveryEncoder,
|
|
14350
14508
|
abortSignal: executionSignal,
|
|
14351
14509
|
assertNotAborted,
|
|
14352
14510
|
onProgress
|
|
@@ -14402,7 +14560,7 @@ async function executeRenderPipeline(input) {
|
|
|
14402
14560
|
"capture_disk",
|
|
14403
14561
|
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
14404
14562
|
);
|
|
14405
|
-
|
|
14563
|
+
rmSync10(framesDir, { recursive: true, force: true });
|
|
14406
14564
|
mkdirSync10(framesDir, { recursive: true });
|
|
14407
14565
|
resetCaptureAttemptProgress(job);
|
|
14408
14566
|
dedupPerfs.length = 0;
|
|
@@ -14500,7 +14658,7 @@ async function executeRenderPipeline(input) {
|
|
|
14500
14658
|
buildArtifactExpectation({
|
|
14501
14659
|
outputFormat,
|
|
14502
14660
|
durationSeconds: job.duration,
|
|
14503
|
-
fps:
|
|
14661
|
+
fps: fpsToNumber8(job.config.fps),
|
|
14504
14662
|
expectedFrames: captureTotalFrames,
|
|
14505
14663
|
width: outputWidth,
|
|
14506
14664
|
height: outputHeight
|
|
@@ -14930,11 +15088,12 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
|
14930
15088
|
import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
|
|
14931
15089
|
import { extname as extname4, isAbsolute as isAbsolute4, join as join30, relative as relative4, sep as sep3 } from "node:path";
|
|
14932
15090
|
import {
|
|
14933
|
-
fpsToNumber as
|
|
15091
|
+
fpsToNumber as fpsToNumber9,
|
|
14934
15092
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
14935
15093
|
} from "@hyperframes/core";
|
|
14936
15094
|
import {
|
|
14937
15095
|
getEncoderPreset as getEncoderPreset4,
|
|
15096
|
+
keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
|
|
14938
15097
|
normalizeVp9CpuUsed,
|
|
14939
15098
|
resolveConfig as resolveConfig3,
|
|
14940
15099
|
declareContentAddressedUrl,
|
|
@@ -15130,6 +15289,12 @@ function assertOptionalInteger(options, field, min = 0) {
|
|
|
15130
15289
|
throw new Error(`Render request ${field} must be an integer >= ${min}`);
|
|
15131
15290
|
}
|
|
15132
15291
|
}
|
|
15292
|
+
function assertOptionalPositiveNumber(options, field) {
|
|
15293
|
+
const value = options[field];
|
|
15294
|
+
if (value !== void 0 && !(typeof value === "number" && value > 0)) {
|
|
15295
|
+
throw new Error(`Render request ${field} must be a positive number`);
|
|
15296
|
+
}
|
|
15297
|
+
}
|
|
15133
15298
|
function assertOptionalString(options, field) {
|
|
15134
15299
|
if (options[field] !== void 0 && typeof options[field] !== "string") {
|
|
15135
15300
|
throw new Error(`Render request ${field} must be a string`);
|
|
@@ -15177,10 +15342,11 @@ function assertRequestOptionScalars(options) {
|
|
|
15177
15342
|
assertOptionalInteger(options, "workers", 1);
|
|
15178
15343
|
assertOptionalInteger(options, "maxRenderMs");
|
|
15179
15344
|
assertOptionalInteger(options, "crf");
|
|
15345
|
+
assertOptionalPositiveNumber(options, "keyframeIntervalSeconds");
|
|
15180
15346
|
for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
|
|
15181
15347
|
assertOptionalBoolean(options, field);
|
|
15182
15348
|
}
|
|
15183
|
-
for (const field of ["entryFile", "videoBitrate"]) {
|
|
15349
|
+
for (const field of ["entryFile", "videoBitrate", "maxBitrate", "bufferSize"]) {
|
|
15184
15350
|
assertOptionalString(options, field);
|
|
15185
15351
|
}
|
|
15186
15352
|
assertOptionalEnum(options, "strictness", ["strict", "best-effort"]);
|
|
@@ -15341,7 +15507,7 @@ function parseRenderOptions(body) {
|
|
|
15341
15507
|
const format = parseServerFormat(body.format);
|
|
15342
15508
|
const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
|
|
15343
15509
|
const encoder = parseEncoderOptions(body, format ?? "mp4");
|
|
15344
|
-
const { crf, videoBitrate, videoFrameFormat } = encoder.ok ? encoder.options : {};
|
|
15510
|
+
const { crf, videoBitrate, videoFrameFormat, keyframeIntervalSeconds, maxBitrate, bufferSize } = encoder.ok ? encoder.options : {};
|
|
15345
15511
|
const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
|
|
15346
15512
|
return {
|
|
15347
15513
|
outputPath,
|
|
@@ -15351,6 +15517,9 @@ function parseRenderOptions(body) {
|
|
|
15351
15517
|
maxRenderMs,
|
|
15352
15518
|
crf,
|
|
15353
15519
|
videoBitrate,
|
|
15520
|
+
keyframeIntervalSeconds,
|
|
15521
|
+
maxBitrate,
|
|
15522
|
+
bufferSize,
|
|
15354
15523
|
useGpu,
|
|
15355
15524
|
debug,
|
|
15356
15525
|
strictness,
|
|
@@ -15385,6 +15554,9 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
15385
15554
|
maxRenderMs: input.maxRenderMs,
|
|
15386
15555
|
crf: input.crf,
|
|
15387
15556
|
videoBitrate: input.videoBitrate,
|
|
15557
|
+
keyframeIntervalSeconds: input.keyframeIntervalSeconds,
|
|
15558
|
+
maxBitrate: input.maxBitrate,
|
|
15559
|
+
bufferSize: input.bufferSize,
|
|
15388
15560
|
useGpu: input.useGpu,
|
|
15389
15561
|
debug: input.debug,
|
|
15390
15562
|
strictness: input.strictness,
|
|
@@ -15531,7 +15703,7 @@ function createArtifactStore(ttlMs) {
|
|
|
15531
15703
|
function cleanupTempDir(dir, log) {
|
|
15532
15704
|
if (!dir) return;
|
|
15533
15705
|
try {
|
|
15534
|
-
|
|
15706
|
+
rmSync11(dir, { recursive: true, force: true });
|
|
15535
15707
|
} catch (error) {
|
|
15536
15708
|
log.warn("Failed to cleanup temp project dir", {
|
|
15537
15709
|
cleanupProjectDir: dir,
|