@hyperframes/producer 0.8.206 → 0.8.208
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 +279 -144
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +76 -71
- package/dist/index.js +607 -357
- package/dist/public-server.js +458 -282
- 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/distributed/renderChunk.d.ts +2 -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/captureStage.d.ts +1 -0
- package/dist/services/render/stages/encodeStage.d.ts +2 -0
- package/dist/services/render/stages/extractVideosStage.d.ts +1 -1
- 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 +7 -0
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
mkdirSync as mkdirSync10,
|
|
6
6
|
readFileSync as readFileSync9,
|
|
7
7
|
readdirSync as readdirSync5,
|
|
8
|
-
rmSync as
|
|
8
|
+
rmSync as rmSync10,
|
|
9
9
|
statSync as statSync8,
|
|
10
10
|
writeFileSync as writeFileSync6,
|
|
11
11
|
copyFileSync as copyFileSync5,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
15
15
|
import {
|
|
16
16
|
chainCapture,
|
|
17
|
-
fpsToNumber as
|
|
17
|
+
fpsToNumber as fpsToNumber8,
|
|
18
18
|
HF_COLOR_GRADING_ATTR,
|
|
19
19
|
HF_VFX_ATTR,
|
|
20
20
|
parseVfxChain,
|
|
@@ -810,7 +810,7 @@ function outputUsesH264Pipeline(format) {
|
|
|
810
810
|
}
|
|
811
811
|
|
|
812
812
|
// src/services/render/hlsConfig.ts
|
|
813
|
-
import {
|
|
813
|
+
import { keyframeIntervalToGopSize } from "@hyperframes/engine";
|
|
814
814
|
var DEFAULT_HLS_SEGMENT_SECONDS = 4;
|
|
815
815
|
function validateHlsRenderConfig(config) {
|
|
816
816
|
if (config.format !== "hls") return;
|
|
@@ -835,7 +835,7 @@ function resolveHlsSegmentSeconds(config) {
|
|
|
835
835
|
return config.hlsSegmentSeconds ?? DEFAULT_HLS_SEGMENT_SECONDS;
|
|
836
836
|
}
|
|
837
837
|
function resolveHlsGopSize(fps, segmentSeconds) {
|
|
838
|
-
return
|
|
838
|
+
return keyframeIntervalToGopSize(fps, segmentSeconds);
|
|
839
839
|
}
|
|
840
840
|
function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
|
|
841
841
|
if (outputFormat !== "hls") return { lockGopForChunkConcat: false, gopSize: void 0 };
|
|
@@ -845,6 +845,41 @@ function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
|
|
|
845
845
|
};
|
|
846
846
|
}
|
|
847
847
|
|
|
848
|
+
// src/services/render/deliveryEncoder.ts
|
|
849
|
+
import { appendRateCapArgs, keyframeIntervalToGopSize as keyframeIntervalToGopSize2 } from "@hyperframes/engine";
|
|
850
|
+
function validateDeliveryEncoderConfig(config) {
|
|
851
|
+
const { keyframeIntervalSeconds, maxBitrate, bufferSize, format } = config;
|
|
852
|
+
if (keyframeIntervalSeconds === void 0 && maxBitrate === void 0 && bufferSize === void 0)
|
|
853
|
+
return;
|
|
854
|
+
if (keyframeIntervalSeconds !== void 0) {
|
|
855
|
+
if (!Number.isFinite(keyframeIntervalSeconds) || keyframeIntervalSeconds <= 0) {
|
|
856
|
+
throw new Error(
|
|
857
|
+
`keyframeIntervalSeconds must be a positive number of seconds; got ${keyframeIntervalSeconds}`
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
if (format === "hls") {
|
|
861
|
+
throw new Error(
|
|
862
|
+
"keyframeIntervalSeconds does not apply to hls; hlsSegmentSeconds sets its keyframes"
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (format !== void 0 && format !== "mp4") {
|
|
867
|
+
throw new Error(`keyframeIntervalSeconds and maxBitrate apply to mp4 renders, not ${format}`);
|
|
868
|
+
}
|
|
869
|
+
appendRateCapArgs([], { maxBitrate, bufferSize });
|
|
870
|
+
}
|
|
871
|
+
function chunkGopFrames(framesInChunk, intervalFrames) {
|
|
872
|
+
return Math.min(framesInChunk, intervalFrames ?? framesInChunk);
|
|
873
|
+
}
|
|
874
|
+
function resolveDeliveryEncoderOptions(config) {
|
|
875
|
+
const { maxBitrate, bufferSize, keyframeIntervalSeconds } = config;
|
|
876
|
+
const gop = keyframeIntervalSeconds === void 0 ? resolveHlsEncoderGopLock(config.format ?? "mp4", config.fps, config.hlsSegmentSeconds) : {
|
|
877
|
+
lockGopForChunkConcat: true,
|
|
878
|
+
gopSize: keyframeIntervalToGopSize2(config.fps, keyframeIntervalSeconds)
|
|
879
|
+
};
|
|
880
|
+
return { maxBitrate, bufferSize, ...gop };
|
|
881
|
+
}
|
|
882
|
+
|
|
848
883
|
// src/services/render/shared.ts
|
|
849
884
|
import {
|
|
850
885
|
copyFileSync,
|
|
@@ -1864,7 +1899,7 @@ function replanSegmentedAfterFailure(plan2, failure) {
|
|
|
1864
1899
|
const keepSegmenting = failure.kind === "draw_element_verification" || failure.kind === "draw_element_capture";
|
|
1865
1900
|
return createCapturePlan({
|
|
1866
1901
|
...plan2,
|
|
1867
|
-
forceScreenshot: keepSegmenting || failure.kind === "capture_failure",
|
|
1902
|
+
forceScreenshot: plan2.forceScreenshot || keepSegmenting || failure.kind === "capture_failure",
|
|
1868
1903
|
useStreamingEncode: true,
|
|
1869
1904
|
useLayeredComposite: false,
|
|
1870
1905
|
useSegmentedCapture: keepSegmenting,
|
|
@@ -1934,7 +1969,7 @@ import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
|
|
|
1934
1969
|
|
|
1935
1970
|
// src/services/render/perfSummary.ts
|
|
1936
1971
|
import { arch, cpus, platform, totalmem } from "node:os";
|
|
1937
|
-
import { fpsToNumber as
|
|
1972
|
+
import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
|
|
1938
1973
|
|
|
1939
1974
|
// src/services/render/hdrPerf.ts
|
|
1940
1975
|
function createHdrPerfCollector() {
|
|
@@ -2205,7 +2240,7 @@ function buildRenderPerfSummary(input) {
|
|
|
2205
2240
|
// payloads (CLI telemetry, regression-harness reports) where a single
|
|
2206
2241
|
// number is friendlier than `{num,den}`. Callers needing the rational
|
|
2207
2242
|
// back can read `job.config.fps`.
|
|
2208
|
-
fps:
|
|
2243
|
+
fps: fpsToNumber2(input.job.config.fps),
|
|
2209
2244
|
quality: input.job.config.quality,
|
|
2210
2245
|
workers: input.workerCount,
|
|
2211
2246
|
workerSizing: input.workerSizing,
|
|
@@ -2790,7 +2825,7 @@ import {
|
|
|
2790
2825
|
resolveVideoExtractionWindow,
|
|
2791
2826
|
runFfmpeg
|
|
2792
2827
|
} from "@hyperframes/engine";
|
|
2793
|
-
import { fpsToFfmpegArg, fpsToNumber as
|
|
2828
|
+
import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
|
|
2794
2829
|
|
|
2795
2830
|
// src/services/hdrFrameWindows.ts
|
|
2796
2831
|
import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
|
|
@@ -3093,7 +3128,7 @@ async function extractHdrVideoFrames(args) {
|
|
|
3093
3128
|
const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
|
|
3094
3129
|
const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
|
|
3095
3130
|
const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
|
|
3096
|
-
const fps =
|
|
3131
|
+
const fps = fpsToNumber3(job.config.fps);
|
|
3097
3132
|
const out = /* @__PURE__ */ new Map();
|
|
3098
3133
|
mkdirSync2(framesDir, { recursive: true });
|
|
3099
3134
|
let scratchBytes = 0;
|
|
@@ -3931,7 +3966,6 @@ async function runCaptureStreamingStage(input) {
|
|
|
3931
3966
|
const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
|
|
3932
3967
|
if (workerCount > 1) {
|
|
3933
3968
|
const deParallelStream = forceParallelStream === true || process.env.HF_DE_PARALLEL_STREAM === "true";
|
|
3934
|
-
const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames, workerCount, workDir) : distributeFrames(totalFrames, workerCount, workDir);
|
|
3935
3969
|
const parallelStats = {
|
|
3936
3970
|
verifyChecked: 0,
|
|
3937
3971
|
blankSuspects: 0,
|
|
@@ -3944,143 +3978,158 @@ async function runCaptureStreamingStage(input) {
|
|
|
3944
3978
|
frameTime: (i) => i * job.config.fps.den / job.config.fps.num
|
|
3945
3979
|
});
|
|
3946
3980
|
let parallelGuardRan = false;
|
|
3947
|
-
let
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3981
|
+
let writtenFrames = 0;
|
|
3982
|
+
let resumeFrom = 0;
|
|
3983
|
+
let resumedAfterStall = false;
|
|
3984
|
+
let workerResults;
|
|
3985
|
+
for (; ; ) {
|
|
3986
|
+
const parallelReorder = createFrameReorderBuffer(resumeFrom, totalFrames);
|
|
3987
|
+
const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames - resumeFrom, workerCount, workDir, resumeFrom) : distributeFrames(totalFrames - resumeFrom, workerCount, workDir, resumeFrom);
|
|
3988
|
+
let parallelDrainError = null;
|
|
3989
|
+
const onFrameBuffer = async (frameIndex, buffer, workerSession) => {
|
|
3990
|
+
try {
|
|
3991
|
+
if (deParallelStream && workerSession.captureMode === "drawelement") {
|
|
3992
|
+
parallelGuardRan = true;
|
|
3993
|
+
buffer = await parallelGuard(workerSession, frameIndex, buffer);
|
|
3994
|
+
}
|
|
3995
|
+
await parallelReorder.waitForFrame(frameIndex);
|
|
3996
|
+
ensureFrameWritten(await currentEncoder.writeFrame(buffer), frameIndex, currentEncoder);
|
|
3997
|
+
parallelReorder.advanceTo(frameIndex + 1);
|
|
3998
|
+
writtenFrames = frameIndex + 1;
|
|
3999
|
+
} catch (err) {
|
|
4000
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
4001
|
+
if (!parallelDrainError) {
|
|
4002
|
+
parallelDrainError = e;
|
|
4003
|
+
parallelReorder.abort(e);
|
|
4004
|
+
}
|
|
4005
|
+
throw e;
|
|
3962
4006
|
}
|
|
3963
|
-
|
|
4007
|
+
};
|
|
4008
|
+
const captureLabel = input.parallelCaptureLabel ?? "worker";
|
|
4009
|
+
let watchdogArmed = false;
|
|
4010
|
+
const stallController = new AbortController();
|
|
4011
|
+
const forwardParentAbort = () => stallController.abort();
|
|
4012
|
+
if (abortSignal) {
|
|
4013
|
+
if (abortSignal.aborted) stallController.abort();
|
|
4014
|
+
else abortSignal.addEventListener("abort", forwardParentAbort, { once: true });
|
|
3964
4015
|
}
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
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
|
-
|
|
4016
|
+
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
4017
|
+
let lastCapturedFrames = 0;
|
|
4018
|
+
let lastProgressAt = Date.now();
|
|
4019
|
+
const workerPhases = /* @__PURE__ */ new Map();
|
|
4020
|
+
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}`;
|
|
4021
|
+
const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
|
|
4022
|
+
let stalled = false;
|
|
4023
|
+
const stallTimer = setInterval(
|
|
4024
|
+
() => {
|
|
4025
|
+
if (!watchdogArmed || Date.now() - lastProgressAt <= stallTimeoutMs) return;
|
|
4026
|
+
stalled = true;
|
|
4027
|
+
const stallErr = new ParallelCaptureStallError({
|
|
4028
|
+
variant: "drain",
|
|
4029
|
+
captureLabel,
|
|
4030
|
+
stallTimeoutMs,
|
|
4031
|
+
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4032
|
+
totalFrames,
|
|
4033
|
+
phaseSummary: phaseSummary()
|
|
4034
|
+
});
|
|
4035
|
+
parallelReorder.abort(stallErr);
|
|
4036
|
+
stallController.abort();
|
|
4037
|
+
},
|
|
4038
|
+
Math.min(stallTimeoutMs, DE_STALL_POLL_MS)
|
|
4039
|
+
);
|
|
4040
|
+
try {
|
|
4041
|
+
workerResults = await executeParallelCapture(
|
|
4042
|
+
fileServer.url,
|
|
4043
|
+
workDir,
|
|
4044
|
+
tasks,
|
|
4045
|
+
buildCaptureOptions(),
|
|
4046
|
+
createRenderVideoFrameInjector,
|
|
4047
|
+
stallController.signal,
|
|
4048
|
+
(progress) => {
|
|
4049
|
+
if (progress.latestWorkerPhase) {
|
|
4050
|
+
const phase = progress.latestWorkerPhase;
|
|
4051
|
+
workerPhases.set(phase.workerId, phaseDetail(phase));
|
|
4052
|
+
if (phase.phase === "frame_capture" && !watchdogArmed) {
|
|
4053
|
+
watchdogArmed = true;
|
|
4054
|
+
lastProgressAt = Date.now();
|
|
4055
|
+
}
|
|
4056
|
+
log.info("[Render] Parallel capture worker phase", {
|
|
4057
|
+
workerId: phase.workerId,
|
|
4058
|
+
phase: phase.phase,
|
|
4059
|
+
frameIndex: phase.frameIndex,
|
|
4060
|
+
browserExecutable: phase.browserExecutable,
|
|
4061
|
+
browserVersion: phase.browserVersion,
|
|
4062
|
+
canvasDrawElement: phase.canvasDrawElement,
|
|
4063
|
+
gpuBackend: phase.gpuBackend
|
|
4064
|
+
});
|
|
4065
|
+
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
if (progress.capturedFrames > lastCapturedFrames) {
|
|
4069
|
+
lastCapturedFrames = progress.capturedFrames;
|
|
4013
4070
|
lastProgressAt = Date.now();
|
|
4071
|
+
watchdogArmed = true;
|
|
4072
|
+
}
|
|
4073
|
+
const doneFrames = resumeFrom + progress.capturedFrames;
|
|
4074
|
+
job.framesRendered = doneFrames;
|
|
4075
|
+
const frameProgress = doneFrames / totalFrames;
|
|
4076
|
+
reportFrameProgress(
|
|
4077
|
+
job,
|
|
4078
|
+
`Streaming frame ${doneFrames}/${totalFrames} (${workerCount} workers)`,
|
|
4079
|
+
Math.round(25 + frameProgress * 55),
|
|
4080
|
+
onProgress,
|
|
4081
|
+
doneFrames,
|
|
4082
|
+
totalFrames
|
|
4083
|
+
);
|
|
4084
|
+
},
|
|
4085
|
+
onFrameBuffer,
|
|
4086
|
+
// Interleaved DE workers each get their own browser process: co-tenant pages starve
|
|
4087
|
+
// each other's paint events.
|
|
4088
|
+
deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
|
|
4089
|
+
{
|
|
4090
|
+
// A worker died: release parked peers with its original failure, not the stall the
|
|
4091
|
+
// watchdog raises later.
|
|
4092
|
+
onWorkerOperation: (phase) => workerPhases.set(phase.workerId, phaseDetail(phase)),
|
|
4093
|
+
onWorkerFailure: (failure) => {
|
|
4094
|
+
if (parallelDrainError) return;
|
|
4095
|
+
parallelDrainError = failure;
|
|
4096
|
+
parallelReorder.abort(failure);
|
|
4014
4097
|
}
|
|
4015
|
-
log.info("[Render] Parallel capture worker phase", {
|
|
4016
|
-
workerId: phase.workerId,
|
|
4017
|
-
phase: phase.phase,
|
|
4018
|
-
frameIndex: phase.frameIndex,
|
|
4019
|
-
browserExecutable: phase.browserExecutable,
|
|
4020
|
-
browserVersion: phase.browserVersion,
|
|
4021
|
-
canvasDrawElement: phase.canvasDrawElement,
|
|
4022
|
-
gpuBackend: phase.gpuBackend
|
|
4023
|
-
});
|
|
4024
|
-
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
4025
|
-
return;
|
|
4026
|
-
}
|
|
4027
|
-
if (progress.capturedFrames > lastCapturedFrames) {
|
|
4028
|
-
lastCapturedFrames = progress.capturedFrames;
|
|
4029
|
-
lastProgressAt = Date.now();
|
|
4030
|
-
watchdogArmed = true;
|
|
4031
4098
|
}
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4099
|
+
);
|
|
4100
|
+
} catch (err) {
|
|
4101
|
+
if (stalled && abortSignal?.aborted !== true && captureLabel === "BeginFrame" && !resumedAfterStall && writtenFrames > resumeFrom) {
|
|
4102
|
+
resumedAfterStall = true;
|
|
4103
|
+
log.warn(
|
|
4104
|
+
"[Render] Parallel BeginFrame capture stalled; resuming from the first unwritten frame",
|
|
4105
|
+
{
|
|
4106
|
+
resumeFrom: writtenFrames,
|
|
4107
|
+
totalFrames,
|
|
4108
|
+
workerPhases: phaseSummary()
|
|
4109
|
+
}
|
|
4041
4110
|
);
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
// Interleaved DE workers each need their own browser PROCESS:
|
|
4045
|
-
// pages co-tenant in one browser share one compositor, whose
|
|
4046
|
-
// internal frame scheduling deprioritizes non-active pages — their
|
|
4047
|
-
// canvas `paint` events starve and the drawElement paint-wait slows
|
|
4048
|
-
// ~3x (measured: 86s vs 30s on a 3,245-frame rAF comp). This is
|
|
4049
|
-
// Chromium's internal frame-production scheduling on ALL platforms,
|
|
4050
|
-
// NOT the Linux-only HeadlessExperimental.beginFrame capture mode.
|
|
4051
|
-
deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
|
|
4052
|
-
{
|
|
4053
|
-
// A worker died (Target closed, Page crashed, launch failure).
|
|
4054
|
-
// Its frames are gone and peers are parked in waitForFrame on the
|
|
4055
|
-
// first of them; release them with the ORIGINAL failure so the
|
|
4056
|
-
// orchestrator sees a transient browser error it knows how to
|
|
4057
|
-
// retry, not the stall the watchdog would have raised a minute
|
|
4058
|
-
// later. First failure wins, same as the drain guard above.
|
|
4059
|
-
onWorkerFailure: (failure) => {
|
|
4060
|
-
if (parallelDrainError) return;
|
|
4061
|
-
parallelDrainError = failure;
|
|
4062
|
-
reorderBuffer.abort(failure);
|
|
4063
|
-
}
|
|
4111
|
+
resumeFrom = writtenFrames;
|
|
4112
|
+
continue;
|
|
4064
4113
|
}
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4114
|
+
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4115
|
+
if (stalled && abortSignal?.aborted !== true) {
|
|
4116
|
+
throw new ParallelCaptureStallError({
|
|
4117
|
+
variant: "pool",
|
|
4118
|
+
captureLabel,
|
|
4119
|
+
stallTimeoutMs,
|
|
4120
|
+
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4121
|
+
totalFrames,
|
|
4122
|
+
phaseSummary: phaseSummary()
|
|
4123
|
+
});
|
|
4124
|
+
}
|
|
4125
|
+
throw poolFailure(err, probeSession);
|
|
4126
|
+
} finally {
|
|
4127
|
+
clearInterval(stallTimer);
|
|
4128
|
+
abortSignal?.removeEventListener("abort", forwardParentAbort);
|
|
4077
4129
|
}
|
|
4078
|
-
throw poolFailure(
|
|
4079
|
-
|
|
4080
|
-
clearInterval(stallTimer);
|
|
4081
|
-
abortSignal?.removeEventListener("abort", forwardParentAbort);
|
|
4130
|
+
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4131
|
+
break;
|
|
4082
4132
|
}
|
|
4083
|
-
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4084
4133
|
pushWorkerDedupPerfs(workerResults, dedupPerfs);
|
|
4085
4134
|
const workerPageErrors = [];
|
|
4086
4135
|
pushWorkerPageErrors(workerResults, workerPageErrors);
|
|
@@ -4241,7 +4290,7 @@ function segmentEncoderOptions(base, segment) {
|
|
|
4241
4290
|
return {
|
|
4242
4291
|
...base,
|
|
4243
4292
|
lockGopForChunkConcat: true,
|
|
4244
|
-
gopSize: segment.endFrame - segment.startFrame
|
|
4293
|
+
gopSize: Math.min(base.gopSize ?? Infinity, segment.endFrame - segment.startFrame)
|
|
4245
4294
|
};
|
|
4246
4295
|
}
|
|
4247
4296
|
var SegmentEncoderSpawnError = class extends Error {
|
|
@@ -4335,6 +4384,12 @@ function defaultSessionFactory(input) {
|
|
|
4335
4384
|
let probeSession = input.probeSession;
|
|
4336
4385
|
return {
|
|
4337
4386
|
create: async () => {
|
|
4387
|
+
const probeCannotCaptureEveryFrame = Boolean(probeSession?.deRiskFrames?.size);
|
|
4388
|
+
if (probeSession && probeCannotCaptureEveryFrame) {
|
|
4389
|
+
await closeCaptureSession2(probeSession).catch(() => {
|
|
4390
|
+
});
|
|
4391
|
+
probeSession = null;
|
|
4392
|
+
}
|
|
4338
4393
|
const session = probeSession ?? await openSegmentedSession(input);
|
|
4339
4394
|
if (probeSession) {
|
|
4340
4395
|
openSegmentedSessionReuse(input, probeSession);
|
|
@@ -4665,8 +4720,10 @@ function computeSegmentPlanHash(input) {
|
|
|
4665
4720
|
input.motionBlur,
|
|
4666
4721
|
input.pageMotionBlur ?? "",
|
|
4667
4722
|
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
4668
|
-
]
|
|
4669
|
-
|
|
4723
|
+
];
|
|
4724
|
+
const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
|
|
4725
|
+
if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
|
|
4726
|
+
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
4670
4727
|
}
|
|
4671
4728
|
function segmentDirFor(projectRendersDir, planHash) {
|
|
4672
4729
|
return join11(projectRendersDir, ".hf-segments", planHash);
|
|
@@ -4813,7 +4870,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
|
4813
4870
|
|
|
4814
4871
|
// src/services/render/captureCost.ts
|
|
4815
4872
|
import { join as join12 } from "node:path";
|
|
4816
|
-
import { fpsToNumber as
|
|
4873
|
+
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
4817
4874
|
import {
|
|
4818
4875
|
calculateOptimalWorkers,
|
|
4819
4876
|
captureFrameToBuffer as captureFrameToBuffer3,
|
|
@@ -5017,7 +5074,7 @@ async function runCaptureCalibration(input) {
|
|
|
5017
5074
|
let probeSession = input.probeSession;
|
|
5018
5075
|
let forceScreenshot = input.forceScreenshot;
|
|
5019
5076
|
let lastBrowserConsole = [];
|
|
5020
|
-
const fps =
|
|
5077
|
+
const fps = fpsToNumber4(job.config.fps);
|
|
5021
5078
|
const sessionRef = { current: null };
|
|
5022
5079
|
const runOneCalibration = async (sessionDir, sessionCfg) => {
|
|
5023
5080
|
log.info("Launching browser for capture calibration...");
|
|
@@ -5477,7 +5534,7 @@ function emitFallbackCaptureProfile(recorder, perfSummaries, env = process.env)
|
|
|
5477
5534
|
// src/services/render/videoFrameCoverage.ts
|
|
5478
5535
|
import { parseHTML } from "linkedom";
|
|
5479
5536
|
import {
|
|
5480
|
-
fpsToNumber as
|
|
5537
|
+
fpsToNumber as fpsToNumber5,
|
|
5481
5538
|
normalizeRateSpec,
|
|
5482
5539
|
sourceTimeAt,
|
|
5483
5540
|
toFps
|
|
@@ -5512,7 +5569,7 @@ function resolveVideoCoverageThreshold(envValue = process.env.HF_VIDEO_COVERAGE_
|
|
|
5512
5569
|
return parsed;
|
|
5513
5570
|
}
|
|
5514
5571
|
function expectedFramesForClip(start, end, fps, rounding = "ceil") {
|
|
5515
|
-
const fpsValue =
|
|
5572
|
+
const fpsValue = fpsToNumber5(toFps(fps));
|
|
5516
5573
|
if (!Number.isFinite(start) || !Number.isFinite(end) || !Number.isFinite(fpsValue)) return 0;
|
|
5517
5574
|
if (fpsValue <= 0) return 0;
|
|
5518
5575
|
const duration = Math.max(0, end - start);
|
|
@@ -6774,7 +6831,7 @@ function detectShaderTransitionUsage(html) {
|
|
|
6774
6831
|
}
|
|
6775
6832
|
return false;
|
|
6776
6833
|
}
|
|
6777
|
-
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
|
|
6834
|
+
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
|
|
6778
6835
|
let filePath = src;
|
|
6779
6836
|
if (isHttpUrl(src)) {
|
|
6780
6837
|
if (!existsSync10(downloadDir)) mkdirSync7(downloadDir, { recursive: true });
|
|
@@ -6801,45 +6858,52 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6801
6858
|
if (error instanceof Error && error.stack) wrapped.stack = error.stack;
|
|
6802
6859
|
return wrapped;
|
|
6803
6860
|
};
|
|
6804
|
-
|
|
6805
|
-
let profile;
|
|
6861
|
+
const probe = () => withMediaProbeSlot(async () => {
|
|
6806
6862
|
try {
|
|
6807
6863
|
await assertMediaPayload(filePath, elementIdentity);
|
|
6808
|
-
profile = await probeMediaProfile2(filePath);
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
if (error instanceof NotMediaPayloadError2) {
|
|
6812
|
-
log?.warn(
|
|
6813
|
-
`[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.`
|
|
6814
|
-
);
|
|
6815
|
-
}
|
|
6816
|
-
return { duration: null, resolvedPath: filePath };
|
|
6864
|
+
const profile = await probeMediaProfile2(filePath);
|
|
6865
|
+
if (tagName === "video") {
|
|
6866
|
+
return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
|
|
6817
6867
|
}
|
|
6818
|
-
throw withSrcContext(error);
|
|
6819
|
-
}
|
|
6820
|
-
assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);
|
|
6821
|
-
let metadata;
|
|
6822
|
-
if (tagName === "video") {
|
|
6823
|
-
try {
|
|
6824
|
-
metadata = await extractMediaMetadata(filePath);
|
|
6825
|
-
} catch (error) {
|
|
6826
|
-
throw withSrcContext(error);
|
|
6827
|
-
}
|
|
6828
|
-
} else {
|
|
6829
6868
|
try {
|
|
6830
|
-
|
|
6869
|
+
return { profile, fileDuration: (await extractAudioMetadata(filePath)).durationSeconds };
|
|
6831
6870
|
} catch {
|
|
6832
|
-
return {
|
|
6871
|
+
return { profile, fileDuration: null };
|
|
6833
6872
|
}
|
|
6873
|
+
} catch (error) {
|
|
6874
|
+
return { error };
|
|
6834
6875
|
}
|
|
6835
|
-
const fileDuration = metadata.durationSeconds;
|
|
6836
|
-
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
6837
|
-
fileDuration,
|
|
6838
|
-
mediaStart,
|
|
6839
|
-
playbackRate
|
|
6840
|
-
);
|
|
6841
|
-
return { duration, resolvedPath: filePath };
|
|
6842
6876
|
});
|
|
6877
|
+
const key = `${tagName}\0${filePath}`;
|
|
6878
|
+
let probed = probes?.get(key);
|
|
6879
|
+
if (!probed) {
|
|
6880
|
+
probed = probe();
|
|
6881
|
+
probes?.set(key, probed);
|
|
6882
|
+
}
|
|
6883
|
+
const outcome = await probed;
|
|
6884
|
+
if ("error" in outcome) {
|
|
6885
|
+
if (tagName !== "video") {
|
|
6886
|
+
if (outcome.error instanceof NotMediaPayloadError2) {
|
|
6887
|
+
log?.warn(
|
|
6888
|
+
`[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.`
|
|
6889
|
+
);
|
|
6890
|
+
}
|
|
6891
|
+
return { duration: null, resolvedPath: filePath };
|
|
6892
|
+
}
|
|
6893
|
+
throw withSrcContext(outcome.error);
|
|
6894
|
+
}
|
|
6895
|
+
assertAssetMediaTypeProfile(
|
|
6896
|
+
tagName === "video" ? "video" : "audio",
|
|
6897
|
+
outcome.profile,
|
|
6898
|
+
elementIdentity
|
|
6899
|
+
);
|
|
6900
|
+
if (outcome.fileDuration === null) return { duration: null, resolvedPath: filePath };
|
|
6901
|
+
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
6902
|
+
outcome.fileDuration,
|
|
6903
|
+
mediaStart,
|
|
6904
|
+
playbackRate
|
|
6905
|
+
);
|
|
6906
|
+
return { duration, resolvedPath: filePath };
|
|
6843
6907
|
}
|
|
6844
6908
|
function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
6845
6909
|
const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
|
|
@@ -6853,7 +6917,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
|
6853
6917
|
}
|
|
6854
6918
|
return changed ? document2.toString() : html;
|
|
6855
6919
|
}
|
|
6856
|
-
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
|
|
6920
|
+
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath, probes) {
|
|
6857
6921
|
const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
|
|
6858
6922
|
const mediaUnresolved = unresolved.filter(
|
|
6859
6923
|
(el) => (el.tagName === "video" || el.tagName === "audio") && el.src
|
|
@@ -6870,7 +6934,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6870
6934
|
el.tagName,
|
|
6871
6935
|
el.id,
|
|
6872
6936
|
log,
|
|
6873
|
-
urlByAbsentLocalPath
|
|
6937
|
+
urlByAbsentLocalPath,
|
|
6938
|
+
probes
|
|
6874
6939
|
).then(({ duration }) => ({ id: el.id, duration }))
|
|
6875
6940
|
)
|
|
6876
6941
|
);
|
|
@@ -6891,7 +6956,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6891
6956
|
downloadDir,
|
|
6892
6957
|
el.tagName,
|
|
6893
6958
|
el.id,
|
|
6894
|
-
log
|
|
6959
|
+
log,
|
|
6960
|
+
void 0,
|
|
6961
|
+
probes
|
|
6895
6962
|
);
|
|
6896
6963
|
return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
|
|
6897
6964
|
})
|
|
@@ -6913,7 +6980,7 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6913
6980
|
compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
|
|
6914
6981
|
return { html: compiledHtml, unresolvedCompositions };
|
|
6915
6982
|
}
|
|
6916
|
-
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
|
|
6983
|
+
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath, probes) {
|
|
6917
6984
|
const subCompositions = /* @__PURE__ */ new Map();
|
|
6918
6985
|
const { document: document2 } = parseHTML5(html);
|
|
6919
6986
|
const compEls = document2.querySelectorAll("[data-composition-src]");
|
|
@@ -6941,14 +7008,16 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
6941
7008
|
dirname7(item.filePath),
|
|
6942
7009
|
downloadDir,
|
|
6943
7010
|
void 0,
|
|
6944
|
-
urlByAbsentLocalPath
|
|
7011
|
+
urlByAbsentLocalPath,
|
|
7012
|
+
probes
|
|
6945
7013
|
);
|
|
6946
7014
|
const nested = await parseSubCompositions(
|
|
6947
7015
|
compiledSub,
|
|
6948
7016
|
projectDir,
|
|
6949
7017
|
downloadDir,
|
|
6950
7018
|
item.nestedVisited,
|
|
6951
|
-
urlByAbsentLocalPath
|
|
7019
|
+
urlByAbsentLocalPath,
|
|
7020
|
+
probes
|
|
6952
7021
|
);
|
|
6953
7022
|
return {
|
|
6954
7023
|
srcPath: item.srcPath,
|
|
@@ -7718,19 +7787,22 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7718
7787
|
);
|
|
7719
7788
|
const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
|
|
7720
7789
|
assertSubCompositionsUsable(rawHtml, projectDir);
|
|
7790
|
+
const probes = /* @__PURE__ */ new Map();
|
|
7721
7791
|
const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
|
|
7722
7792
|
rawHtml,
|
|
7723
7793
|
projectDir,
|
|
7724
7794
|
downloadDir,
|
|
7725
7795
|
options.log,
|
|
7726
|
-
options.urlByAbsentLocalPath
|
|
7796
|
+
options.urlByAbsentLocalPath,
|
|
7797
|
+
probes
|
|
7727
7798
|
);
|
|
7728
7799
|
const { subCompositions } = await parseSubCompositions(
|
|
7729
7800
|
compiledHtml,
|
|
7730
7801
|
projectDir,
|
|
7731
7802
|
downloadDir,
|
|
7732
7803
|
void 0,
|
|
7733
|
-
options.urlByAbsentLocalPath
|
|
7804
|
+
options.urlByAbsentLocalPath,
|
|
7805
|
+
probes
|
|
7734
7806
|
);
|
|
7735
7807
|
const fullHtml = ensureFullDocument(compiledHtml);
|
|
7736
7808
|
const inlinedHtml = inlineSubCompositions(
|
|
@@ -7833,9 +7905,12 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7833
7905
|
}
|
|
7834
7906
|
const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
|
|
7835
7907
|
const masterFxChain = parseMasterFxChain(html);
|
|
7908
|
+
const advised = /* @__PURE__ */ new Set();
|
|
7836
7909
|
for (const video of videos) {
|
|
7837
7910
|
if (isHttpUrl(video.src)) continue;
|
|
7838
7911
|
const videoPath = resolve7(projectDir, video.src);
|
|
7912
|
+
if (advised.has(videoPath)) continue;
|
|
7913
|
+
advised.add(videoPath);
|
|
7839
7914
|
const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
|
|
7840
7915
|
(async () => {
|
|
7841
7916
|
const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
|
|
@@ -8386,13 +8461,14 @@ import {
|
|
|
8386
8461
|
} from "@hyperframes/engine";
|
|
8387
8462
|
import {
|
|
8388
8463
|
MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
|
|
8389
|
-
fpsToNumber as
|
|
8464
|
+
fpsToNumber as fpsToNumber6,
|
|
8390
8465
|
normalizeRateSpec as normalizeRateSpec3,
|
|
8391
8466
|
readMediaStart as readMediaStart2,
|
|
8392
8467
|
shiftRateLane as shiftRateLane2,
|
|
8393
8468
|
sourceTimeAt as sourceTimeAt3,
|
|
8394
8469
|
timeAtSourceTime
|
|
8395
8470
|
} from "@hyperframes/core";
|
|
8471
|
+
import { parseHostVariableValues as parseHostVariableValues2, readDeclaredDefaults as readDeclaredDefaults2 } from "@hyperframes/core/compiler";
|
|
8396
8472
|
import { extractMediaSrcMutations } from "@hyperframes/parsers";
|
|
8397
8473
|
|
|
8398
8474
|
// src/services/render/stages/probeFailures.ts
|
|
@@ -8430,8 +8506,15 @@ function hasAutoStartVideos(html) {
|
|
|
8430
8506
|
const { document: document2 } = parseHTML6(html);
|
|
8431
8507
|
return document2.querySelector("video[data-hf-auto-start]") !== null;
|
|
8432
8508
|
}
|
|
8509
|
+
function variableHasValue(element, variableId, variables) {
|
|
8510
|
+
if (variables && Object.hasOwn(variables, variableId)) return true;
|
|
8511
|
+
for (let scope = element.parentElement; scope; scope = scope.parentElement) {
|
|
8512
|
+
if (Object.hasOwn(parseHostVariableValues2(scope), variableId)) return true;
|
|
8513
|
+
if (Object.hasOwn(readDeclaredDefaults2(scope), variableId)) return true;
|
|
8514
|
+
}
|
|
8515
|
+
return Object.hasOwn(readDeclaredDefaults2(element.ownerDocument.documentElement), variableId);
|
|
8516
|
+
}
|
|
8433
8517
|
function hasVariableBoundMedia(html, variables) {
|
|
8434
|
-
if (!variables || Object.keys(variables).length === 0) return false;
|
|
8435
8518
|
const { document: document2 } = parseHTML6(html);
|
|
8436
8519
|
return Array.from(
|
|
8437
8520
|
document2.querySelectorAll(
|
|
@@ -8439,7 +8522,7 @@ function hasVariableBoundMedia(html, variables) {
|
|
|
8439
8522
|
)
|
|
8440
8523
|
).some((element) => {
|
|
8441
8524
|
const variableId = element.getAttribute("data-var-src")?.trim();
|
|
8442
|
-
return Boolean(variableId &&
|
|
8525
|
+
return Boolean(variableId && variableHasValue(element, variableId, variables));
|
|
8443
8526
|
});
|
|
8444
8527
|
}
|
|
8445
8528
|
function authoredMediaStarts(html) {
|
|
@@ -8865,7 +8948,7 @@ async function runProbeStage(input) {
|
|
|
8865
8948
|
session.page,
|
|
8866
8949
|
composition.audios.map((audio) => audio.id),
|
|
8867
8950
|
composition.duration,
|
|
8868
|
-
|
|
8951
|
+
fpsToNumber6(job.config.fps)
|
|
8869
8952
|
);
|
|
8870
8953
|
assertNotAborted();
|
|
8871
8954
|
if (automation.length > 0) {
|
|
@@ -8928,7 +9011,7 @@ async function runProbeStage(input) {
|
|
|
8928
9011
|
}
|
|
8929
9012
|
const browserProbeMs = Date.now() - probeStart;
|
|
8930
9013
|
const duration = composition.duration;
|
|
8931
|
-
const totalFrames = durationToFrameCount(duration,
|
|
9014
|
+
const totalFrames = durationToFrameCount(duration, fpsToNumber6(job.config.fps));
|
|
8932
9015
|
if (duration <= 0) {
|
|
8933
9016
|
const diagnostics = [];
|
|
8934
9017
|
try {
|
|
@@ -9173,10 +9256,11 @@ var VideoExtractionStageError = class extends Error {
|
|
|
9173
9256
|
kind: failure.kind,
|
|
9174
9257
|
affectedElementCount: failure.count
|
|
9175
9258
|
}))
|
|
9176
|
-
)) {
|
|
9259
|
+
), ffmpegReason) {
|
|
9177
9260
|
const total = failures.reduce((sum2, failure) => sum2 + failure.count, 0);
|
|
9178
9261
|
const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
|
|
9179
|
-
|
|
9262
|
+
const reason = ffmpegReason ? `: ffmpeg said: ${ffmpegReason}` : "";
|
|
9263
|
+
super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]${reason}`);
|
|
9180
9264
|
this.code = code;
|
|
9181
9265
|
this.retryable = retryable;
|
|
9182
9266
|
this.failures = failures;
|
|
@@ -9223,7 +9307,8 @@ function buildVideoExtractionStageError(result) {
|
|
|
9223
9307
|
retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE",
|
|
9224
9308
|
retryable,
|
|
9225
9309
|
failures,
|
|
9226
|
-
extractionFailureMetadataFromResult(result)
|
|
9310
|
+
extractionFailureMetadataFromResult(result),
|
|
9311
|
+
result.errors.find((failure) => failure.ffmpegReason)?.ffmpegReason
|
|
9227
9312
|
);
|
|
9228
9313
|
}
|
|
9229
9314
|
function buildHdrProbeStageError(failures) {
|
|
@@ -9464,16 +9549,17 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
9464
9549
|
|
|
9465
9550
|
// src/services/render/stages/audioStage.ts
|
|
9466
9551
|
import { join as join20 } from "node:path";
|
|
9467
|
-
import { rmSync as rmSync10 } from "node:fs";
|
|
9468
9552
|
import {
|
|
9469
9553
|
MIXED_AUDIO_FILENAME,
|
|
9470
9554
|
audioFailureDiagnostics,
|
|
9471
|
-
processCompositionAudio
|
|
9555
|
+
processCompositionAudio,
|
|
9556
|
+
removeScratch as removeScratch2,
|
|
9557
|
+
collectScratchLeftovers
|
|
9472
9558
|
} from "@hyperframes/engine";
|
|
9473
9559
|
|
|
9474
9560
|
// src/services/render/audioPadTrim.ts
|
|
9475
9561
|
import { spawn } from "node:child_process";
|
|
9476
|
-
import { mkdtempSync as mkdtempSync3
|
|
9562
|
+
import { mkdtempSync as mkdtempSync3 } from "node:fs";
|
|
9477
9563
|
import { dirname as dirname8, join as join19 } from "node:path";
|
|
9478
9564
|
import {
|
|
9479
9565
|
DEFAULT_CONFIG,
|
|
@@ -9482,6 +9568,7 @@ import {
|
|
|
9482
9568
|
extractAudioMetadata as extractAudioMetadata3,
|
|
9483
9569
|
getFfprobeBinary,
|
|
9484
9570
|
ManagedChildProcess,
|
|
9571
|
+
removeScratch,
|
|
9485
9572
|
renameWithRetry as renameWithRetry5,
|
|
9486
9573
|
runFfmpeg as runFfmpeg5,
|
|
9487
9574
|
trackChildProcess
|
|
@@ -9677,7 +9764,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
9677
9764
|
error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
9678
9765
|
};
|
|
9679
9766
|
} finally {
|
|
9680
|
-
for (const path of plan2.cleanupPaths)
|
|
9767
|
+
for (const path of plan2.cleanupPaths) removeScratch(path);
|
|
9681
9768
|
}
|
|
9682
9769
|
let loweredDb = 0;
|
|
9683
9770
|
if (probeTruePeak) {
|
|
@@ -9740,7 +9827,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9740
9827
|
}
|
|
9741
9828
|
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
9742
9829
|
if (measuredPath !== correctedPath) return { success: true };
|
|
9743
|
-
renameWithRetry5(correctedPath, input.audioPath);
|
|
9830
|
+
renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
|
|
9744
9831
|
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
9745
9832
|
}
|
|
9746
9833
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
@@ -9781,7 +9868,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9781
9868
|
)}`
|
|
9782
9869
|
};
|
|
9783
9870
|
} finally {
|
|
9784
|
-
if (scratchDir)
|
|
9871
|
+
if (scratchDir) removeScratch(scratchDir);
|
|
9785
9872
|
}
|
|
9786
9873
|
}
|
|
9787
9874
|
function failResult(outputPath, target, source, error) {
|
|
@@ -9926,7 +10013,15 @@ function assertAudioNotInterrupted(failures) {
|
|
|
9926
10013
|
])
|
|
9927
10014
|
});
|
|
9928
10015
|
}
|
|
9929
|
-
|
|
10016
|
+
function runAudioStage(input) {
|
|
10017
|
+
return collectScratchLeftovers(
|
|
10018
|
+
() => runAudioStageScoped(input),
|
|
10019
|
+
(left) => {
|
|
10020
|
+
if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
|
|
10021
|
+
}
|
|
10022
|
+
);
|
|
10023
|
+
}
|
|
10024
|
+
async function runAudioStageScoped(input) {
|
|
9930
10025
|
const stageStart = Date.now();
|
|
9931
10026
|
const pcmSourcePath = join20(input.workDir, "audio-source.wav");
|
|
9932
10027
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
@@ -9948,7 +10043,7 @@ async function runAudioStage(input) {
|
|
|
9948
10043
|
}
|
|
9949
10044
|
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
9950
10045
|
} finally {
|
|
9951
|
-
|
|
10046
|
+
removeScratch2(pcmSourcePath);
|
|
9952
10047
|
}
|
|
9953
10048
|
}
|
|
9954
10049
|
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
@@ -10210,6 +10305,7 @@ async function runCaptureStage(input) {
|
|
|
10210
10305
|
dedupPerfs,
|
|
10211
10306
|
pageErrorLines: workerPageErrors,
|
|
10212
10307
|
openAttempt: input.openAttempt,
|
|
10308
|
+
verifyFirstSession: input.verifyFirstSession,
|
|
10213
10309
|
onProgress: (progress) => {
|
|
10214
10310
|
if (progress.latestWorkerPhase) {
|
|
10215
10311
|
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
@@ -10428,7 +10524,7 @@ import {
|
|
|
10428
10524
|
initializeSession as initializeSession7,
|
|
10429
10525
|
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
10430
10526
|
} from "@hyperframes/engine";
|
|
10431
|
-
import { fpsToNumber as
|
|
10527
|
+
import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
|
|
10432
10528
|
|
|
10433
10529
|
// src/services/hdrImageTransferCache.ts
|
|
10434
10530
|
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
@@ -11102,6 +11198,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11102
11198
|
preset,
|
|
11103
11199
|
effectiveQuality,
|
|
11104
11200
|
effectiveBitrate,
|
|
11201
|
+
deliveryEncoder,
|
|
11105
11202
|
fileServer,
|
|
11106
11203
|
buildCaptureOptions,
|
|
11107
11204
|
createRenderVideoFrameInjector,
|
|
@@ -11163,7 +11260,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11163
11260
|
}
|
|
11164
11261
|
return map;
|
|
11165
11262
|
});
|
|
11166
|
-
const fpsDecimal =
|
|
11263
|
+
const fpsDecimal = fpsToNumber7(job.config.fps);
|
|
11167
11264
|
const transitionRanges = transitionMeta.map((t) => ({
|
|
11168
11265
|
...t,
|
|
11169
11266
|
startFrame: Math.floor(t.time * fpsDecimal),
|
|
@@ -11191,6 +11288,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11191
11288
|
preset: preset.preset,
|
|
11192
11289
|
quality: effectiveQuality,
|
|
11193
11290
|
bitrate: effectiveBitrate,
|
|
11291
|
+
...deliveryEncoder,
|
|
11194
11292
|
pixelFormat: preset.pixelFormat,
|
|
11195
11293
|
hdr: preset.hdr,
|
|
11196
11294
|
rawInputFormat: "rgb48le"
|
|
@@ -11250,7 +11348,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11250
11348
|
beforeCaptureHook: domSession.onBeforeCapture,
|
|
11251
11349
|
width,
|
|
11252
11350
|
height,
|
|
11253
|
-
fps:
|
|
11351
|
+
fps: fpsToNumber7(job.config.fps),
|
|
11254
11352
|
compositeTransfer,
|
|
11255
11353
|
nativeHdrImageIds,
|
|
11256
11354
|
hdrImageBuffers,
|
|
@@ -11394,7 +11492,7 @@ import {
|
|
|
11394
11492
|
mkdirSync as mkdirSync9,
|
|
11395
11493
|
readdirSync as readdirSync4,
|
|
11396
11494
|
readFileSync as readFileSync8,
|
|
11397
|
-
rmSync as
|
|
11495
|
+
rmSync as rmSync9,
|
|
11398
11496
|
statSync as statSync7,
|
|
11399
11497
|
writeFileSync as writeFileSync5
|
|
11400
11498
|
} from "node:fs";
|
|
@@ -11533,7 +11631,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
11533
11631
|
fileSize
|
|
11534
11632
|
};
|
|
11535
11633
|
} finally {
|
|
11536
|
-
|
|
11634
|
+
rmSync9(input.palettePath, { force: true });
|
|
11537
11635
|
}
|
|
11538
11636
|
}
|
|
11539
11637
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -11557,6 +11655,8 @@ function videoEncoderOptions(input, engineCfg) {
|
|
|
11557
11655
|
preset: preset.preset,
|
|
11558
11656
|
quality: input.effectiveQuality,
|
|
11559
11657
|
bitrate: input.effectiveBitrate,
|
|
11658
|
+
maxBitrate: input.maxBitrate,
|
|
11659
|
+
bufferSize: input.bufferSize,
|
|
11560
11660
|
pixelFormat: preset.pixelFormat,
|
|
11561
11661
|
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
11562
11662
|
useGpu: job.config.useGpu,
|
|
@@ -12265,7 +12365,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
12265
12365
|
});
|
|
12266
12366
|
} : void 0,
|
|
12267
12367
|
void 0,
|
|
12268
|
-
options.cfg
|
|
12368
|
+
options.cfg,
|
|
12369
|
+
{ verifyFirstSession: options.verifyFirstSession }
|
|
12269
12370
|
);
|
|
12270
12371
|
pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
|
|
12271
12372
|
if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
|
|
@@ -12831,6 +12932,7 @@ function resolveRenderDebugDir() {
|
|
|
12831
12932
|
}
|
|
12832
12933
|
async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
|
|
12833
12934
|
validateHlsRenderConfig(job.config);
|
|
12935
|
+
validateDeliveryEncoderConfig(job.config);
|
|
12834
12936
|
const debugDir = resolveRenderDebugDir();
|
|
12835
12937
|
const outputDir = dirname11(outputPath);
|
|
12836
12938
|
if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
|
|
@@ -12851,7 +12953,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
12851
12953
|
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
12852
12954
|
return;
|
|
12853
12955
|
}
|
|
12854
|
-
|
|
12956
|
+
rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
12855
12957
|
});
|
|
12856
12958
|
try {
|
|
12857
12959
|
await executeRenderPipeline({
|
|
@@ -12909,11 +13011,14 @@ async function executeRenderPipeline(input) {
|
|
|
12909
13011
|
const isGif = outputFormat === "gif";
|
|
12910
13012
|
const isHls = outputFormat === "hls";
|
|
12911
13013
|
const hlsSegmentSeconds = isHls ? resolveHlsSegmentSeconds(job.config) : void 0;
|
|
12912
|
-
const
|
|
12913
|
-
outputFormat,
|
|
12914
|
-
job.config.fps,
|
|
12915
|
-
job.config.hlsSegmentSeconds
|
|
12916
|
-
|
|
13014
|
+
const deliveryEncoder = resolveDeliveryEncoderOptions({
|
|
13015
|
+
format: outputFormat,
|
|
13016
|
+
fps: job.config.fps,
|
|
13017
|
+
hlsSegmentSeconds: job.config.hlsSegmentSeconds,
|
|
13018
|
+
keyframeIntervalSeconds: job.config.keyframeIntervalSeconds,
|
|
13019
|
+
maxBitrate: job.config.maxBitrate,
|
|
13020
|
+
bufferSize: job.config.bufferSize
|
|
13021
|
+
});
|
|
12917
13022
|
const artifactTransaction = new ArtifactTransaction(
|
|
12918
13023
|
outputPath,
|
|
12919
13024
|
isPngSequence || isHls ? "directory" : "file",
|
|
@@ -13153,7 +13258,7 @@ async function executeRenderPipeline(input) {
|
|
|
13153
13258
|
validateRenderDuration({
|
|
13154
13259
|
duration: probeResult.duration,
|
|
13155
13260
|
totalFrames,
|
|
13156
|
-
fps:
|
|
13261
|
+
fps: fpsToNumber8(job.config.fps)
|
|
13157
13262
|
});
|
|
13158
13263
|
perfStages.browserProbeMs = probeResult.browserProbeMs;
|
|
13159
13264
|
perfStages.compileMs = Date.now() - stage1Start;
|
|
@@ -13996,6 +14101,7 @@ async function executeRenderPipeline(input) {
|
|
|
13996
14101
|
preset,
|
|
13997
14102
|
effectiveQuality,
|
|
13998
14103
|
effectiveBitrate,
|
|
14104
|
+
deliveryEncoder,
|
|
13999
14105
|
fileServer: activeFileServer,
|
|
14000
14106
|
buildCaptureOptions,
|
|
14001
14107
|
createRenderVideoFrameInjector,
|
|
@@ -14020,9 +14126,7 @@ async function executeRenderPipeline(input) {
|
|
|
14020
14126
|
} else {
|
|
14021
14127
|
let streamingHandled = false;
|
|
14022
14128
|
if (capturePlan.kind === "sdr_segmented") {
|
|
14023
|
-
const segmentedPlan = capturePlan;
|
|
14024
14129
|
const captureFrameStart = Date.now();
|
|
14025
|
-
resetCaptureAttemptProgress(job);
|
|
14026
14130
|
const segmentFrames = resolveSegmentFrames(process.env);
|
|
14027
14131
|
const segmentImageFormat = captureOptions.format || "jpeg";
|
|
14028
14132
|
const segmentPlanHash = computeSegmentPlanHash({
|
|
@@ -14037,6 +14141,9 @@ async function executeRenderPipeline(input) {
|
|
|
14037
14141
|
preset: preset.preset,
|
|
14038
14142
|
quality: effectiveQuality,
|
|
14039
14143
|
bitrate: effectiveBitrate,
|
|
14144
|
+
maxBitrate: deliveryEncoder.maxBitrate,
|
|
14145
|
+
bufferSize: deliveryEncoder.bufferSize,
|
|
14146
|
+
gopSize: deliveryEncoder.gopSize,
|
|
14040
14147
|
pixelFormat: preset.pixelFormat,
|
|
14041
14148
|
imageFormat: segmentImageFormat,
|
|
14042
14149
|
useGpu: job.config.useGpu === true,
|
|
@@ -14062,71 +14169,129 @@ async function executeRenderPipeline(input) {
|
|
|
14062
14169
|
});
|
|
14063
14170
|
}
|
|
14064
14171
|
if (completedSegments.size === 0) {
|
|
14065
|
-
|
|
14172
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14066
14173
|
}
|
|
14067
14174
|
} else {
|
|
14068
|
-
|
|
14175
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14069
14176
|
}
|
|
14070
|
-
const
|
|
14177
|
+
const emptySegmentManifest = () => ({
|
|
14071
14178
|
version: 1,
|
|
14072
14179
|
planHash: segmentPlanHash,
|
|
14073
14180
|
totalFrames,
|
|
14074
14181
|
segmentFrames,
|
|
14075
14182
|
completed: []
|
|
14183
|
+
});
|
|
14184
|
+
let segmentManifest = readSegmentManifest(segmentDir) ?? emptySegmentManifest();
|
|
14185
|
+
const invokeSegmented = () => {
|
|
14186
|
+
if (capturePlan.kind !== "sdr_segmented") {
|
|
14187
|
+
throw new Error(`Cannot invoke segmented stage with ${capturePlan.kind} plan`);
|
|
14188
|
+
}
|
|
14189
|
+
const segmentedPlan = capturePlan;
|
|
14190
|
+
resetCaptureAttemptProgress(job);
|
|
14191
|
+
return observeRenderStage(
|
|
14192
|
+
observability,
|
|
14193
|
+
"capture_segmented",
|
|
14194
|
+
captureStageObservationData(),
|
|
14195
|
+
() => runCaptureSegmentedStage({
|
|
14196
|
+
fileServer: activeFileServer,
|
|
14197
|
+
workDir,
|
|
14198
|
+
framesDir,
|
|
14199
|
+
videoOnlyPath,
|
|
14200
|
+
job,
|
|
14201
|
+
totalFrames,
|
|
14202
|
+
cfg,
|
|
14203
|
+
plan: segmentedPlan,
|
|
14204
|
+
log,
|
|
14205
|
+
probeSession,
|
|
14206
|
+
outputFormat,
|
|
14207
|
+
streamingEncoderOptions: {
|
|
14208
|
+
fps: job.config.fps,
|
|
14209
|
+
width: captureCompositionWidth ?? width,
|
|
14210
|
+
height: captureCompositionHeight ?? height,
|
|
14211
|
+
codec: preset.codec,
|
|
14212
|
+
preset: preset.preset,
|
|
14213
|
+
quality: effectiveQuality,
|
|
14214
|
+
bitrate: effectiveBitrate,
|
|
14215
|
+
pixelFormat: preset.pixelFormat,
|
|
14216
|
+
vp9CpuUsed: cfg.vp9CpuUsed,
|
|
14217
|
+
useGpu: job.config.useGpu,
|
|
14218
|
+
imageFormat: segmentImageFormat,
|
|
14219
|
+
hdr: preset.hdr,
|
|
14220
|
+
// Each segment sets its own GOP (at most the interval); hls never reaches this route.
|
|
14221
|
+
...deliveryEncoder
|
|
14222
|
+
},
|
|
14223
|
+
buildCaptureOptions,
|
|
14224
|
+
createRenderVideoFrameInjector,
|
|
14225
|
+
abortSignal: executionSignal,
|
|
14226
|
+
assertNotAborted,
|
|
14227
|
+
onProgress,
|
|
14228
|
+
dedupPerfs,
|
|
14229
|
+
segmentFrames,
|
|
14230
|
+
segmentDir,
|
|
14231
|
+
workerCount: segmentedPlan.workerCount,
|
|
14232
|
+
browserRecycleEverySegments: resolveSegmentBrowserRecycle(process.env),
|
|
14233
|
+
completedSegments,
|
|
14234
|
+
onSegmentComplete: (entry) => {
|
|
14235
|
+
segmentManifest.completed = [
|
|
14236
|
+
...segmentManifest.completed.filter((e) => e.index !== entry.index),
|
|
14237
|
+
{ ...entry, completedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
14238
|
+
];
|
|
14239
|
+
writeSegmentManifest(segmentDir, segmentManifest);
|
|
14240
|
+
},
|
|
14241
|
+
updateCaptureObservability
|
|
14242
|
+
})
|
|
14243
|
+
);
|
|
14076
14244
|
};
|
|
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
|
-
|
|
14112
|
-
|
|
14113
|
-
|
|
14114
|
-
|
|
14115
|
-
|
|
14116
|
-
|
|
14117
|
-
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14122
|
-
|
|
14123
|
-
|
|
14124
|
-
|
|
14125
|
-
|
|
14126
|
-
|
|
14127
|
-
updateCaptureObservability
|
|
14128
|
-
})
|
|
14129
|
-
);
|
|
14245
|
+
let segmentedRes;
|
|
14246
|
+
try {
|
|
14247
|
+
segmentedRes = await invokeSegmented();
|
|
14248
|
+
} catch (err) {
|
|
14249
|
+
const isVerifyError = isDrawElementVerificationError(err);
|
|
14250
|
+
const isDeStall = isDeRendererStallError(err);
|
|
14251
|
+
if (!isVerifyError && !isDrawElementCaptureError(err) && !isDeStall || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
|
|
14252
|
+
throw err;
|
|
14253
|
+
}
|
|
14254
|
+
deSelfVerifyFallback = isVerifyError;
|
|
14255
|
+
const t = deVerifyFallbackTelemetry(err);
|
|
14256
|
+
deFallbackReason = isVerifyError ? t.reason : isDeStall ? "de_renderer_stall" : "capture_error";
|
|
14257
|
+
deFallbackFailedDb = t.failedDb;
|
|
14258
|
+
deFallbackFrameIndex = t.frameIndex;
|
|
14259
|
+
deFallbackThresholdDb = t.thresholdDb;
|
|
14260
|
+
log.warn(
|
|
14261
|
+
"[Render] drawElement capture failed on the segmented path; re-rendering via screenshot",
|
|
14262
|
+
{
|
|
14263
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14264
|
+
}
|
|
14265
|
+
);
|
|
14266
|
+
observability.checkpoint(
|
|
14267
|
+
"capture_segmented",
|
|
14268
|
+
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
14269
|
+
);
|
|
14270
|
+
if (probeSession) {
|
|
14271
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
14272
|
+
const orphaned = probeSession;
|
|
14273
|
+
probeSession = null;
|
|
14274
|
+
await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
|
|
14275
|
+
}
|
|
14276
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14277
|
+
completedSegments = /* @__PURE__ */ new Set();
|
|
14278
|
+
segmentManifest = emptySegmentManifest();
|
|
14279
|
+
dedupPerfs.length = 0;
|
|
14280
|
+
capturePlan = replanAfterFailure(capturePlan, {
|
|
14281
|
+
kind: isVerifyError ? "draw_element_verification" : "draw_element_capture"
|
|
14282
|
+
});
|
|
14283
|
+
syncCapturePlan();
|
|
14284
|
+
updateCaptureObservability({
|
|
14285
|
+
forceScreenshot: capturePlan.forceScreenshot,
|
|
14286
|
+
deSelfVerifyFallback,
|
|
14287
|
+
deFallbackReason,
|
|
14288
|
+
deFallbackFailedDb,
|
|
14289
|
+
deFallbackFrameIndex,
|
|
14290
|
+
deFallbackThresholdDb
|
|
14291
|
+
});
|
|
14292
|
+
segmentedRes = await invokeSegmented();
|
|
14293
|
+
observability.clearFailure("capture_segmented");
|
|
14294
|
+
}
|
|
14130
14295
|
if (segmentedRes.success) {
|
|
14131
14296
|
streamingHandled = true;
|
|
14132
14297
|
workerCount = segmentedRes.workerCount;
|
|
@@ -14146,7 +14311,7 @@ async function executeRenderPipeline(input) {
|
|
|
14146
14311
|
);
|
|
14147
14312
|
updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
|
|
14148
14313
|
if (job.config.keepSegments !== true) {
|
|
14149
|
-
|
|
14314
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14150
14315
|
}
|
|
14151
14316
|
} else {
|
|
14152
14317
|
capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
|
|
@@ -14199,7 +14364,7 @@ async function executeRenderPipeline(input) {
|
|
|
14199
14364
|
// assemble stage's `-c copy` segmentation cuts exactly on
|
|
14200
14365
|
// the segment boundary. Every other format leaves the
|
|
14201
14366
|
// encoder's open-GOP output untouched.
|
|
14202
|
-
...
|
|
14367
|
+
...deliveryEncoder
|
|
14203
14368
|
},
|
|
14204
14369
|
buildCaptureOptions,
|
|
14205
14370
|
createRenderVideoFrameInjector,
|
|
@@ -14362,7 +14527,7 @@ async function executeRenderPipeline(input) {
|
|
|
14362
14527
|
engineConfig: cfg,
|
|
14363
14528
|
// Same value the streaming encoder above spreads — the disk and
|
|
14364
14529
|
// chunked-concat encoders need an identical GOP lock for HLS.
|
|
14365
|
-
...
|
|
14530
|
+
...deliveryEncoder,
|
|
14366
14531
|
abortSignal: executionSignal,
|
|
14367
14532
|
assertNotAborted,
|
|
14368
14533
|
onProgress
|
|
@@ -14418,7 +14583,7 @@ async function executeRenderPipeline(input) {
|
|
|
14418
14583
|
"capture_disk",
|
|
14419
14584
|
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
14420
14585
|
);
|
|
14421
|
-
|
|
14586
|
+
rmSync10(framesDir, { recursive: true, force: true });
|
|
14422
14587
|
mkdirSync10(framesDir, { recursive: true });
|
|
14423
14588
|
resetCaptureAttemptProgress(job);
|
|
14424
14589
|
dedupPerfs.length = 0;
|
|
@@ -14516,7 +14681,7 @@ async function executeRenderPipeline(input) {
|
|
|
14516
14681
|
buildArtifactExpectation({
|
|
14517
14682
|
outputFormat,
|
|
14518
14683
|
durationSeconds: job.duration,
|
|
14519
|
-
fps:
|
|
14684
|
+
fps: fpsToNumber8(job.config.fps),
|
|
14520
14685
|
expectedFrames: captureTotalFrames,
|
|
14521
14686
|
width: outputWidth,
|
|
14522
14687
|
height: outputHeight
|
|
@@ -14735,16 +14900,17 @@ import {
|
|
|
14735
14900
|
existsSync as existsSync21,
|
|
14736
14901
|
linkSync,
|
|
14737
14902
|
mkdirSync as mkdirSync14,
|
|
14738
|
-
rmSync as
|
|
14903
|
+
rmSync as rmSync12,
|
|
14739
14904
|
writeFileSync as writeFileSync9
|
|
14740
14905
|
} from "node:fs";
|
|
14741
14906
|
import { extname as extname6, isAbsolute as isAbsolute3, join as join35, relative as relative5, sep as sep2 } from "node:path";
|
|
14742
14907
|
import {
|
|
14743
|
-
fpsToNumber as
|
|
14908
|
+
fpsToNumber as fpsToNumber9,
|
|
14744
14909
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
14745
14910
|
} from "@hyperframes/core";
|
|
14746
14911
|
import {
|
|
14747
14912
|
getEncoderPreset as getEncoderPreset4,
|
|
14913
|
+
keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
|
|
14748
14914
|
normalizeVp9CpuUsed,
|
|
14749
14915
|
resolveConfig as resolveConfig3,
|
|
14750
14916
|
declareContentAddressedUrl,
|
|
@@ -15770,7 +15936,7 @@ import { join as join33 } from "node:path";
|
|
|
15770
15936
|
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
|
|
15771
15937
|
|
|
15772
15938
|
// src/services/distributed/mp4Stitch.ts
|
|
15773
|
-
import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync4, rmSync as
|
|
15939
|
+
import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync4, rmSync as rmSync11 } from "node:fs";
|
|
15774
15940
|
import { open } from "node:fs/promises";
|
|
15775
15941
|
import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
|
|
15776
15942
|
import { dirname as dirname13, join as join32 } from "node:path";
|
|
@@ -16148,7 +16314,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
|
16148
16314
|
renameWithRetry6(join32(workDir, "out.mp4"), outputPath);
|
|
16149
16315
|
} finally {
|
|
16150
16316
|
out.destroy();
|
|
16151
|
-
|
|
16317
|
+
rmSync11(workDir, { recursive: true, force: true });
|
|
16152
16318
|
}
|
|
16153
16319
|
}
|
|
16154
16320
|
|
|
@@ -16656,7 +16822,7 @@ var FREEZE_OWNED_PLAN_FILES = [
|
|
|
16656
16822
|
];
|
|
16657
16823
|
function removeFreezeOwnedPlanFiles(planDir) {
|
|
16658
16824
|
for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
|
|
16659
|
-
|
|
16825
|
+
rmSync12(join35(planDir, relativePath), { force: true });
|
|
16660
16826
|
}
|
|
16661
16827
|
}
|
|
16662
16828
|
function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
@@ -16716,6 +16882,9 @@ function buildLockedRenderConfig(input) {
|
|
|
16716
16882
|
preset,
|
|
16717
16883
|
crf: config.crf,
|
|
16718
16884
|
bitrate: config.bitrate,
|
|
16885
|
+
maxBitrate: config.maxBitrate,
|
|
16886
|
+
bufferSize: config.bufferSize,
|
|
16887
|
+
keyframeIntervalFrames: config.keyframeIntervalSeconds === void 0 ? void 0 : keyframeIntervalToGopSize3({ num: config.fps, den: 1 }, config.keyframeIntervalSeconds),
|
|
16719
16888
|
...locksVp9CpuUsed,
|
|
16720
16889
|
// GOP === chunkSize so every chunk's first frame is an IDR keyframe and
|
|
16721
16890
|
// ffmpeg concat-copy round-trips losslessly.
|
|
@@ -16867,7 +17036,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
16867
17036
|
throw new Error(`[plan] entry file not found: ${htmlPath}`);
|
|
16868
17037
|
}
|
|
16869
17038
|
const workDir = join35(planDir, ".plan-work");
|
|
16870
|
-
|
|
17039
|
+
rmSync12(workDir, { recursive: true, force: true });
|
|
16871
17040
|
mkdirSync14(workDir, { recursive: true });
|
|
16872
17041
|
const compiledDir = join35(workDir, "compiled");
|
|
16873
17042
|
mkdirSync14(compiledDir, { recursive: true });
|
|
@@ -16960,7 +17129,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
16960
17129
|
validateDistributedDuration({
|
|
16961
17130
|
duration: probeResult.duration,
|
|
16962
17131
|
totalFrames,
|
|
16963
|
-
fps:
|
|
17132
|
+
fps: fpsToNumber9(job.config.fps)
|
|
16964
17133
|
});
|
|
16965
17134
|
if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
|
|
16966
17135
|
if (probeResult.probeSession) {
|
|
@@ -17074,15 +17243,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
17074
17243
|
logPlanStage(log, "extract", stageStarted);
|
|
17075
17244
|
const stagedVideoFrames = join35(compiledDir, "__hyperframes_video_frames");
|
|
17076
17245
|
const videoFramesDst = join35(planDir, "video-frames");
|
|
17077
|
-
if (existsSync21(videoFramesDst))
|
|
17246
|
+
if (existsSync21(videoFramesDst)) rmSync12(videoFramesDst, { recursive: true, force: true });
|
|
17078
17247
|
const videoSourcesDir = join35(planDir, "video-sources");
|
|
17079
|
-
|
|
17248
|
+
rmSync12(videoSourcesDir, { recursive: true, force: true });
|
|
17080
17249
|
if (existsSync21(stagedVideoFrames)) {
|
|
17081
17250
|
renameWithRetry7(stagedVideoFrames, videoFramesDst);
|
|
17082
17251
|
} else {
|
|
17083
17252
|
mkdirSync14(videoFramesDst, { recursive: true });
|
|
17084
17253
|
}
|
|
17085
|
-
if (existsSync21(finalCompiledDir))
|
|
17254
|
+
if (existsSync21(finalCompiledDir)) rmSync12(finalCompiledDir, { recursive: true, force: true });
|
|
17086
17255
|
renameWithRetry7(compiledDir, finalCompiledDir);
|
|
17087
17256
|
const shipVideoSource = createVideoSourceShipper({
|
|
17088
17257
|
planDir,
|
|
@@ -17250,7 +17419,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
17250
17419
|
format: config.format
|
|
17251
17420
|
};
|
|
17252
17421
|
try {
|
|
17253
|
-
|
|
17422
|
+
rmSync12(workDir, { recursive: true, force: true });
|
|
17254
17423
|
} catch (err) {
|
|
17255
17424
|
log.warn("[plan] failed to remove temp work dir", {
|
|
17256
17425
|
workDir,
|
|
@@ -17401,6 +17570,20 @@ function validateDistributedRenderConfig(config) {
|
|
|
17401
17570
|
`must look like "10M" or "5000k"; got ${JSON.stringify(config.bitrate)}`
|
|
17402
17571
|
);
|
|
17403
17572
|
}
|
|
17573
|
+
try {
|
|
17574
|
+
validateDeliveryEncoderConfig({
|
|
17575
|
+
format: config.format,
|
|
17576
|
+
fps: { num: config.fps, den: 1 },
|
|
17577
|
+
keyframeIntervalSeconds: config.keyframeIntervalSeconds,
|
|
17578
|
+
maxBitrate: config.maxBitrate,
|
|
17579
|
+
bufferSize: config.bufferSize
|
|
17580
|
+
});
|
|
17581
|
+
} catch (error) {
|
|
17582
|
+
throw new InvalidConfigError(
|
|
17583
|
+
"config.maxBitrate",
|
|
17584
|
+
error instanceof Error ? error.message : String(error)
|
|
17585
|
+
);
|
|
17586
|
+
}
|
|
17404
17587
|
if (config.chunkSize !== void 0) {
|
|
17405
17588
|
if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
|
|
17406
17589
|
throw new InvalidConfigError(
|
|
@@ -17580,6 +17763,12 @@ function assertOptionalInteger(options, field, min = 0) {
|
|
|
17580
17763
|
throw new Error(`Render request ${field} must be an integer >= ${min}`);
|
|
17581
17764
|
}
|
|
17582
17765
|
}
|
|
17766
|
+
function assertOptionalPositiveNumber(options, field) {
|
|
17767
|
+
const value = options[field];
|
|
17768
|
+
if (value !== void 0 && !(typeof value === "number" && value > 0)) {
|
|
17769
|
+
throw new Error(`Render request ${field} must be a positive number`);
|
|
17770
|
+
}
|
|
17771
|
+
}
|
|
17583
17772
|
function assertOptionalString(options, field) {
|
|
17584
17773
|
if (options[field] !== void 0 && typeof options[field] !== "string") {
|
|
17585
17774
|
throw new Error(`Render request ${field} must be a string`);
|
|
@@ -17627,10 +17816,11 @@ function assertRequestOptionScalars(options) {
|
|
|
17627
17816
|
assertOptionalInteger(options, "workers", 1);
|
|
17628
17817
|
assertOptionalInteger(options, "maxRenderMs");
|
|
17629
17818
|
assertOptionalInteger(options, "crf");
|
|
17819
|
+
assertOptionalPositiveNumber(options, "keyframeIntervalSeconds");
|
|
17630
17820
|
for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
|
|
17631
17821
|
assertOptionalBoolean(options, field);
|
|
17632
17822
|
}
|
|
17633
|
-
for (const field of ["entryFile", "videoBitrate"]) {
|
|
17823
|
+
for (const field of ["entryFile", "videoBitrate", "maxBitrate", "bufferSize"]) {
|
|
17634
17824
|
assertOptionalString(options, field);
|
|
17635
17825
|
}
|
|
17636
17826
|
assertOptionalEnum(options, "strictness", ["strict", "best-effort"]);
|
|
@@ -17733,6 +17923,9 @@ function distributedConfigFromRequest(request, runtime = {}) {
|
|
|
17733
17923
|
crf: options.crf,
|
|
17734
17924
|
pageMotionBlur: options.pageMotionBlur,
|
|
17735
17925
|
bitrate: options.videoBitrate,
|
|
17926
|
+
keyframeIntervalSeconds: options.keyframeIntervalSeconds,
|
|
17927
|
+
maxBitrate: options.maxBitrate,
|
|
17928
|
+
bufferSize: options.bufferSize,
|
|
17736
17929
|
videoFrameFormat: options.videoFrameFormat,
|
|
17737
17930
|
outputResolution: options.outputResolution,
|
|
17738
17931
|
outputResolutionAspectAgnostic: options.outputResolutionAspectAgnostic,
|
|
@@ -17780,6 +17973,9 @@ function renderRequestFromDistributedConfig(input) {
|
|
|
17780
17973
|
...optionalProperty("crf", config.crf),
|
|
17781
17974
|
...optionalProperty("pageMotionBlur", config.pageMotionBlur),
|
|
17782
17975
|
...optionalProperty("videoBitrate", config.bitrate),
|
|
17976
|
+
...optionalProperty("keyframeIntervalSeconds", config.keyframeIntervalSeconds),
|
|
17977
|
+
...optionalProperty("maxBitrate", config.maxBitrate),
|
|
17978
|
+
...optionalProperty("bufferSize", config.bufferSize),
|
|
17783
17979
|
...optionalProperty("videoFrameFormat", config.videoFrameFormat),
|
|
17784
17980
|
...optionalProperty("outputResolution", config.outputResolution),
|
|
17785
17981
|
...optionalProperty("outputResolutionAspectAgnostic", config.outputResolutionAspectAgnostic),
|
|
@@ -17825,7 +18021,7 @@ import {
|
|
|
17825
18021
|
statSync as statSync10,
|
|
17826
18022
|
mkdtempSync as mkdtempSync5,
|
|
17827
18023
|
writeFileSync as writeFileSync10,
|
|
17828
|
-
rmSync as
|
|
18024
|
+
rmSync as rmSync13,
|
|
17829
18025
|
createReadStream as createReadStream5
|
|
17830
18026
|
} from "node:fs";
|
|
17831
18027
|
import { resolve as resolve11, dirname as dirname15, join as join38 } from "node:path";
|
|
@@ -18131,7 +18327,7 @@ function parseRenderOptions(body) {
|
|
|
18131
18327
|
const format = parseServerFormat(body.format);
|
|
18132
18328
|
const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
|
|
18133
18329
|
const encoder = parseEncoderOptions(body, format ?? "mp4");
|
|
18134
|
-
const { crf, videoBitrate, videoFrameFormat } = encoder.ok ? encoder.options : {};
|
|
18330
|
+
const { crf, videoBitrate, videoFrameFormat, keyframeIntervalSeconds, maxBitrate, bufferSize } = encoder.ok ? encoder.options : {};
|
|
18135
18331
|
const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
|
|
18136
18332
|
return {
|
|
18137
18333
|
outputPath,
|
|
@@ -18141,6 +18337,9 @@ function parseRenderOptions(body) {
|
|
|
18141
18337
|
maxRenderMs,
|
|
18142
18338
|
crf,
|
|
18143
18339
|
videoBitrate,
|
|
18340
|
+
keyframeIntervalSeconds,
|
|
18341
|
+
maxBitrate,
|
|
18342
|
+
bufferSize,
|
|
18144
18343
|
useGpu,
|
|
18145
18344
|
debug,
|
|
18146
18345
|
strictness,
|
|
@@ -18175,6 +18374,9 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
18175
18374
|
maxRenderMs: input.maxRenderMs,
|
|
18176
18375
|
crf: input.crf,
|
|
18177
18376
|
videoBitrate: input.videoBitrate,
|
|
18377
|
+
keyframeIntervalSeconds: input.keyframeIntervalSeconds,
|
|
18378
|
+
maxBitrate: input.maxBitrate,
|
|
18379
|
+
bufferSize: input.bufferSize,
|
|
18178
18380
|
useGpu: input.useGpu,
|
|
18179
18381
|
debug: input.debug,
|
|
18180
18382
|
strictness: input.strictness,
|
|
@@ -18321,7 +18523,7 @@ function createArtifactStore(ttlMs) {
|
|
|
18321
18523
|
function cleanupTempDir(dir, log) {
|
|
18322
18524
|
if (!dir) return;
|
|
18323
18525
|
try {
|
|
18324
|
-
|
|
18526
|
+
rmSync13(dir, { recursive: true, force: true });
|
|
18325
18527
|
} catch (error) {
|
|
18326
18528
|
log.warn("Failed to cleanup temp project dir", {
|
|
18327
18529
|
cleanupProjectDir: dir,
|
|
@@ -18735,7 +18937,7 @@ import {
|
|
|
18735
18937
|
mkdtempSync as mkdtempSync7,
|
|
18736
18938
|
readFileSync as readFileSync14,
|
|
18737
18939
|
readdirSync as readdirSync9,
|
|
18738
|
-
rmSync as
|
|
18940
|
+
rmSync as rmSync15,
|
|
18739
18941
|
statSync as statSync12,
|
|
18740
18942
|
writeFileSync as writeFileSync12
|
|
18741
18943
|
} from "node:fs";
|
|
@@ -18751,7 +18953,7 @@ import {
|
|
|
18751
18953
|
linkSync as linkSync2,
|
|
18752
18954
|
mkdirSync as mkdirSync16,
|
|
18753
18955
|
mkdtempSync as mkdtempSync6,
|
|
18754
|
-
rmSync as
|
|
18956
|
+
rmSync as rmSync14,
|
|
18755
18957
|
statSync as statSync11,
|
|
18756
18958
|
writeFileSync as writeFileSync11
|
|
18757
18959
|
} from "node:fs";
|
|
@@ -18846,7 +19048,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
18846
19048
|
}
|
|
18847
19049
|
renameWithRetry8(temporaryPath, destinationPath);
|
|
18848
19050
|
} finally {
|
|
18849
|
-
|
|
19051
|
+
rmSync14(stagingDir, { recursive: true, force: true });
|
|
18850
19052
|
}
|
|
18851
19053
|
}
|
|
18852
19054
|
async commitManifest(manifestBytes) {
|
|
@@ -18863,7 +19065,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
18863
19065
|
}
|
|
18864
19066
|
async abort() {
|
|
18865
19067
|
if (!this.#committed) {
|
|
18866
|
-
|
|
19068
|
+
rmSync14(this.temporaryDir, { recursive: true, force: true });
|
|
18867
19069
|
}
|
|
18868
19070
|
}
|
|
18869
19071
|
};
|
|
@@ -19132,7 +19334,7 @@ function writeBlob(sourcePath, destinationPath) {
|
|
|
19132
19334
|
copyFileSync8(sourcePath, temporaryPath);
|
|
19133
19335
|
renameWithRetry9(temporaryPath, destinationPath);
|
|
19134
19336
|
} finally {
|
|
19135
|
-
|
|
19337
|
+
rmSync15(temporaryDir, { recursive: true, force: true });
|
|
19136
19338
|
}
|
|
19137
19339
|
}
|
|
19138
19340
|
function publishBlobFor(absolutePath, digests) {
|
|
@@ -19265,7 +19467,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
|
19265
19467
|
renameWithRetry9(tempDir, planV2Dir);
|
|
19266
19468
|
return resultFromManifest(planV2Dir, publication.manifest);
|
|
19267
19469
|
} catch (error) {
|
|
19268
|
-
|
|
19470
|
+
rmSync15(tempDir, { recursive: true, force: true });
|
|
19269
19471
|
throw error;
|
|
19270
19472
|
}
|
|
19271
19473
|
}
|
|
@@ -19332,7 +19534,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
19332
19534
|
}
|
|
19333
19535
|
throw error;
|
|
19334
19536
|
} finally {
|
|
19335
|
-
|
|
19537
|
+
rmSync15(stagingRoot, { recursive: true, force: true });
|
|
19336
19538
|
}
|
|
19337
19539
|
}
|
|
19338
19540
|
async function planV2(projectDir, config, planV2Dir) {
|
|
@@ -19549,7 +19751,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
19549
19751
|
);
|
|
19550
19752
|
renameWithRetry9(tempDir, destinationDir);
|
|
19551
19753
|
} catch (error) {
|
|
19552
|
-
|
|
19754
|
+
rmSync15(tempDir, { recursive: true, force: true });
|
|
19553
19755
|
throw error;
|
|
19554
19756
|
}
|
|
19555
19757
|
return {
|
|
@@ -19613,7 +19815,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
19613
19815
|
}
|
|
19614
19816
|
|
|
19615
19817
|
// src/services/distributed/planV2Execution.ts
|
|
19616
|
-
import { mkdtempSync as mkdtempSync9, rmSync as
|
|
19818
|
+
import { mkdtempSync as mkdtempSync9, rmSync as rmSync18 } from "node:fs";
|
|
19617
19819
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
19618
19820
|
import { join as join44 } from "node:path";
|
|
19619
19821
|
|
|
@@ -19625,12 +19827,13 @@ import {
|
|
|
19625
19827
|
mkdtempSync as mkdtempSync8,
|
|
19626
19828
|
readFileSync as readFileSync15,
|
|
19627
19829
|
readdirSync as readdirSync10,
|
|
19628
|
-
rmSync as
|
|
19830
|
+
rmSync as rmSync16,
|
|
19629
19831
|
statSync as statSync13,
|
|
19630
19832
|
writeFileSync as writeFileSync13
|
|
19631
19833
|
} from "node:fs";
|
|
19632
19834
|
import { dirname as dirname18, join as join42 } from "node:path";
|
|
19633
19835
|
import {
|
|
19836
|
+
appendRateCapArgs as appendRateCapArgs2,
|
|
19634
19837
|
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
19635
19838
|
describeFfmpegFailure as describeFfmpegFailure4,
|
|
19636
19839
|
getEncoderPreset as getEncoderPreset5,
|
|
@@ -19704,7 +19907,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
19704
19907
|
});
|
|
19705
19908
|
} finally {
|
|
19706
19909
|
try {
|
|
19707
|
-
|
|
19910
|
+
rmSync16(workDir, { recursive: true, force: true });
|
|
19708
19911
|
} catch (err) {
|
|
19709
19912
|
log.warn("[assemble] failed to remove work dir", {
|
|
19710
19913
|
workDir,
|
|
@@ -19766,6 +19969,9 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
|
|
|
19766
19969
|
"-r",
|
|
19767
19970
|
fpsArg
|
|
19768
19971
|
];
|
|
19972
|
+
if (encoder.keyframeIntervalFrames !== void 0)
|
|
19973
|
+
args.push("-g", String(encoder.keyframeIntervalFrames));
|
|
19974
|
+
appendRateCapArgs2(args, encoder);
|
|
19769
19975
|
appendSoftwareEncoderThreadsArg(args, "libx264");
|
|
19770
19976
|
return args;
|
|
19771
19977
|
}
|
|
@@ -19902,7 +20108,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
19902
20108
|
return encoderJson;
|
|
19903
20109
|
}
|
|
19904
20110
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
19905
|
-
if (existsSync27(outputPath))
|
|
20111
|
+
if (existsSync27(outputPath)) rmSync16(outputPath, { recursive: true, force: true });
|
|
19906
20112
|
mkdirSync18(outputPath, { recursive: true });
|
|
19907
20113
|
let globalIdx = 0;
|
|
19908
20114
|
for (const chunkDir of chunkPaths) {
|
|
@@ -19947,7 +20153,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
19947
20153
|
|
|
19948
20154
|
// src/services/distributed/renderChunk.ts
|
|
19949
20155
|
import { randomBytes } from "node:crypto";
|
|
19950
|
-
import { existsSync as existsSync28, mkdirSync as mkdirSync19, readFileSync as readFileSync16, readdirSync as readdirSync11, rmSync as
|
|
20156
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync19, readFileSync as readFileSync16, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync14 } from "node:fs";
|
|
19951
20157
|
import { cpus as cpus3 } from "node:os";
|
|
19952
20158
|
import { join as join43 } from "node:path";
|
|
19953
20159
|
import {
|
|
@@ -20043,16 +20249,18 @@ function observedChunkCaptureMode(perfs) {
|
|
|
20043
20249
|
}
|
|
20044
20250
|
return mode;
|
|
20045
20251
|
}
|
|
20252
|
+
var PROBE_SELECTED_SCREENSHOT = "[renderChunk] probe selected screenshot capture";
|
|
20046
20253
|
function shouldRetryChunkCaptureWithScreenshot(error) {
|
|
20047
20254
|
const failure = classifyCaptureFailure4(error);
|
|
20048
20255
|
if (failure.kind === "cancelled" || failure.kind === "memory_exhaustion") return false;
|
|
20049
|
-
return /HeadlessExperimental\.beginFrame/i.test(failure.message) || /beginFrame probe timeout/i.test(failure.message) || /Another frame is pending|Frame still pending/i.test(failure.message);
|
|
20256
|
+
return /HeadlessExperimental\.beginFrame/i.test(failure.message) || /beginFrame probe timeout/i.test(failure.message) || failure.message.includes(PROBE_SELECTED_SCREENSHOT) || /Another frame is pending|Frame still pending/i.test(failure.message);
|
|
20050
20257
|
}
|
|
20051
20258
|
async function runCaptureWithScreenshotFallback(input) {
|
|
20052
20259
|
try {
|
|
20053
20260
|
return await input.run(input.forceScreenshot);
|
|
20054
20261
|
} catch (error) {
|
|
20055
|
-
if (input.forceScreenshot ||
|
|
20262
|
+
if (input.forceScreenshot || await input.canRetry?.() === false) throw error;
|
|
20263
|
+
if (!shouldRetryChunkCaptureWithScreenshot(error)) throw error;
|
|
20056
20264
|
input.onFallback?.(error);
|
|
20057
20265
|
await input.resetForScreenshotRetry();
|
|
20058
20266
|
return await input.run(true);
|
|
@@ -20261,7 +20469,24 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20261
20469
|
let extraction;
|
|
20262
20470
|
const overlap = { current: null };
|
|
20263
20471
|
try {
|
|
20264
|
-
|
|
20472
|
+
const verifyInWorker = chunkWorkerCount > 1 && !forceScreenshotForChunk;
|
|
20473
|
+
let boxesMeasured;
|
|
20474
|
+
let boxesFailed;
|
|
20475
|
+
const boxesReady = new Promise((resolve13, reject) => {
|
|
20476
|
+
boxesMeasured = resolve13;
|
|
20477
|
+
boxesFailed = reject;
|
|
20478
|
+
});
|
|
20479
|
+
boxesReady.catch(() => {
|
|
20480
|
+
});
|
|
20481
|
+
const measureBoxes = async (page) => {
|
|
20482
|
+
shownBoxes = await measureVideoShownBoxes(
|
|
20483
|
+
page,
|
|
20484
|
+
[...shownFrames.keys()],
|
|
20485
|
+
chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
|
|
20486
|
+
);
|
|
20487
|
+
boxesMeasured();
|
|
20488
|
+
};
|
|
20489
|
+
if (chunkWorkerCount === 1) {
|
|
20265
20490
|
const bootStarted = Date.now();
|
|
20266
20491
|
session = await createVerifiedDistributedCaptureSession(
|
|
20267
20492
|
fileServer.url,
|
|
@@ -20270,11 +20495,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20270
20495
|
cfg
|
|
20271
20496
|
);
|
|
20272
20497
|
sessionBootMs = Date.now() - bootStarted;
|
|
20273
|
-
|
|
20274
|
-
session.page,
|
|
20275
|
-
[...shownFrames.keys()],
|
|
20276
|
-
chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
|
|
20277
|
-
);
|
|
20498
|
+
await measureBoxes(session.page);
|
|
20278
20499
|
const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
|
|
20279
20500
|
const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
|
|
20280
20501
|
if (browserSelectedScreenshot) {
|
|
@@ -20283,10 +20504,6 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20283
20504
|
"[renderChunk] Browser capability probe selected screenshot capture for the entire chunk",
|
|
20284
20505
|
{ chunkIndex }
|
|
20285
20506
|
);
|
|
20286
|
-
if (chunkWorkerCount > 1) {
|
|
20287
|
-
await closeCaptureSession11(session);
|
|
20288
|
-
session = null;
|
|
20289
|
-
}
|
|
20290
20507
|
} else if (beginFrameStalled) {
|
|
20291
20508
|
forceScreenshotForChunk = true;
|
|
20292
20509
|
log.warn(
|
|
@@ -20296,41 +20513,66 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20296
20513
|
await closeCaptureSession11(session).catch(() => {
|
|
20297
20514
|
});
|
|
20298
20515
|
session = null;
|
|
20299
|
-
|
|
20300
|
-
|
|
20301
|
-
|
|
20302
|
-
|
|
20303
|
-
|
|
20304
|
-
|
|
20305
|
-
|
|
20306
|
-
|
|
20307
|
-
|
|
20308
|
-
|
|
20309
|
-
|
|
20310
|
-
|
|
20516
|
+
const screenshotBootStarted = Date.now();
|
|
20517
|
+
const screenshotCfg = {
|
|
20518
|
+
...cfg,
|
|
20519
|
+
forceScreenshot: true,
|
|
20520
|
+
forceScreenshotExplicitlyOptedOut: false
|
|
20521
|
+
};
|
|
20522
|
+
session = await createVerifiedDistributedCaptureSession(
|
|
20523
|
+
fileServer.url,
|
|
20524
|
+
framesDir,
|
|
20525
|
+
captureOptions,
|
|
20526
|
+
screenshotCfg
|
|
20527
|
+
);
|
|
20528
|
+
sessionBootMs += Date.now() - screenshotBootStarted;
|
|
20529
|
+
}
|
|
20530
|
+
} else if (!verifyInWorker) {
|
|
20531
|
+
boxesMeasured();
|
|
20532
|
+
}
|
|
20533
|
+
let verification;
|
|
20534
|
+
const verifyFirstWorker = (first) => verification ??= (async () => {
|
|
20535
|
+
sessionBootMs = Date.now() - verifyStarted;
|
|
20536
|
+
try {
|
|
20537
|
+
await measureBoxes(first.page);
|
|
20538
|
+
const selectedScreenshot = first.launchCaptureMode === "screenshot";
|
|
20539
|
+
if (selectedScreenshot || await beginFrameSessionNeedsScreenshotFallback(first)) {
|
|
20540
|
+
forceScreenshotForChunk = true;
|
|
20541
|
+
log.warn(
|
|
20542
|
+
selectedScreenshot ? "[renderChunk] Browser capability probe selected screenshot capture for the entire chunk" : "[renderChunk] BeginFrame liveness probe failed; using screenshot capture for the entire chunk",
|
|
20543
|
+
{ chunkIndex }
|
|
20311
20544
|
);
|
|
20312
|
-
|
|
20545
|
+
throw new Error(PROBE_SELECTED_SCREENSHOT);
|
|
20313
20546
|
}
|
|
20314
|
-
}
|
|
20315
|
-
|
|
20316
|
-
|
|
20547
|
+
} catch (error) {
|
|
20548
|
+
if (!forceScreenshotForChunk) {
|
|
20549
|
+
verifyFailure = error;
|
|
20550
|
+
boxesFailed(error);
|
|
20551
|
+
}
|
|
20552
|
+
throw error;
|
|
20317
20553
|
}
|
|
20318
|
-
}
|
|
20319
|
-
|
|
20320
|
-
|
|
20321
|
-
|
|
20322
|
-
|
|
20323
|
-
|
|
20324
|
-
|
|
20325
|
-
|
|
20326
|
-
|
|
20327
|
-
|
|
20328
|
-
|
|
20329
|
-
|
|
20330
|
-
|
|
20331
|
-
|
|
20332
|
-
|
|
20554
|
+
})();
|
|
20555
|
+
let verifyStarted = 0;
|
|
20556
|
+
let verifyFailure;
|
|
20557
|
+
const deferredVideoFrames = planVideos ? boxesReady.then(() => {
|
|
20558
|
+
const extractStarted = Date.now();
|
|
20559
|
+
return extractDeferredVideoFramesForChunk({
|
|
20560
|
+
planDir,
|
|
20561
|
+
planVideos,
|
|
20562
|
+
frames: chunkFrameNumbers,
|
|
20563
|
+
fps: chunkFps,
|
|
20564
|
+
outputRoot: videoFramesDir,
|
|
20565
|
+
cfg,
|
|
20566
|
+
remoteSources,
|
|
20567
|
+
shownBoxes
|
|
20568
|
+
}).then((frames) => {
|
|
20569
|
+
videoExtractMs = Date.now() - extractStarted;
|
|
20570
|
+
return frames;
|
|
20571
|
+
});
|
|
20572
|
+
}) : boxesReady.then(() => []);
|
|
20333
20573
|
extraction = deferredVideoFrames;
|
|
20574
|
+
deferredVideoFrames.catch(() => {
|
|
20575
|
+
});
|
|
20334
20576
|
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
|
|
20335
20577
|
(deferred) => createFrameLookupTable5(
|
|
20336
20578
|
planVideos.videos,
|
|
@@ -20402,11 +20644,14 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20402
20644
|
// first frame is an IDR keyframe and concat-copy at assemble time
|
|
20403
20645
|
// round-trips losslessly.
|
|
20404
20646
|
lockGopForChunkConcat: !isPngSequence,
|
|
20405
|
-
gopSize: framesInChunk
|
|
20647
|
+
gopSize: chunkGopFrames(framesInChunk, encoder.keyframeIntervalFrames),
|
|
20648
|
+
maxBitrate: encoder.maxBitrate,
|
|
20649
|
+
bufferSize: encoder.bufferSize
|
|
20406
20650
|
};
|
|
20407
20651
|
const captureStarted = Date.now();
|
|
20408
20652
|
captureMode = await runCaptureWithScreenshotFallback({
|
|
20409
20653
|
forceScreenshot: forceScreenshotForChunk,
|
|
20654
|
+
canRetry: () => verifyFailure === void 0,
|
|
20410
20655
|
// fallow-ignore-next-line complexity
|
|
20411
20656
|
run: async (forceScreenshot) => {
|
|
20412
20657
|
const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : {
|
|
@@ -20436,6 +20681,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20436
20681
|
}
|
|
20437
20682
|
const captureSession = session;
|
|
20438
20683
|
session = null;
|
|
20684
|
+
verifyStarted = Date.now();
|
|
20439
20685
|
overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus3().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
|
|
20440
20686
|
await observeRenderStage(
|
|
20441
20687
|
new RenderObservabilityRecorder({ pipelineStartMs: start, log }),
|
|
@@ -20463,7 +20709,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20463
20709
|
frameRange: range,
|
|
20464
20710
|
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
|
|
20465
20711
|
interleaveFrames: interleavesCapture(overlap.current !== null, options),
|
|
20466
|
-
openAttempt
|
|
20712
|
+
openAttempt,
|
|
20713
|
+
verifyFirstSession: forceScreenshot || !verifyInWorker ? void 0 : verifyFirstWorker
|
|
20467
20714
|
}),
|
|
20468
20715
|
{ stall: frameStallRule(job) }
|
|
20469
20716
|
).catch(async (captureError) => {
|
|
@@ -20474,7 +20721,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20474
20721
|
return observedChunkCaptureMode(capturePerfs);
|
|
20475
20722
|
},
|
|
20476
20723
|
resetForScreenshotRetry: () => {
|
|
20477
|
-
|
|
20724
|
+
rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
20725
|
+
boxesMeasured();
|
|
20478
20726
|
mkdirSync19(framesDir, { recursive: true });
|
|
20479
20727
|
capturePerfs.length = 0;
|
|
20480
20728
|
captureAttempts.length = 0;
|
|
@@ -20490,6 +20738,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20490
20738
|
);
|
|
20491
20739
|
}
|
|
20492
20740
|
}).catch(async (captureError) => {
|
|
20741
|
+
if (verifyFailure !== void 0) throw verifyFailure;
|
|
20742
|
+
boxesFailed(captureError);
|
|
20493
20743
|
await deferredVideoFrames;
|
|
20494
20744
|
throw captureError;
|
|
20495
20745
|
});
|
|
@@ -20517,8 +20767,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20517
20767
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
20518
20768
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
20519
20769
|
});
|
|
20520
|
-
|
|
20521
|
-
|
|
20770
|
+
rmSync17(videoOnlyPath, { force: true });
|
|
20771
|
+
rmSync17(audioScratchPath, { force: true });
|
|
20522
20772
|
}
|
|
20523
20773
|
encodeStageMs = Date.now() - encodeStarted;
|
|
20524
20774
|
} finally {
|
|
@@ -20568,8 +20818,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20568
20818
|
writeFileSync14(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
20569
20819
|
`, "utf-8");
|
|
20570
20820
|
try {
|
|
20571
|
-
|
|
20572
|
-
|
|
20821
|
+
rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
20822
|
+
rmSync17(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
20573
20823
|
} catch (err) {
|
|
20574
20824
|
log.warn("[renderChunk] failed to remove work dir", {
|
|
20575
20825
|
workDir,
|
|
@@ -20604,7 +20854,7 @@ async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
|
|
|
20604
20854
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
20605
20855
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
|
|
20606
20856
|
} finally {
|
|
20607
|
-
|
|
20857
|
+
rmSync18(workRoot, { recursive: true, force: true });
|
|
20608
20858
|
}
|
|
20609
20859
|
}
|
|
20610
20860
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
@@ -20624,7 +20874,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
20624
20874
|
options
|
|
20625
20875
|
);
|
|
20626
20876
|
} finally {
|
|
20627
|
-
|
|
20877
|
+
rmSync18(workRoot, { recursive: true, force: true });
|
|
20628
20878
|
}
|
|
20629
20879
|
}
|
|
20630
20880
|
|