@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/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 {
|
|
@@ -9162,10 +9242,11 @@ var VideoExtractionStageError = class extends Error {
|
|
|
9162
9242
|
kind: failure.kind,
|
|
9163
9243
|
affectedElementCount: failure.count
|
|
9164
9244
|
}))
|
|
9165
|
-
)) {
|
|
9245
|
+
), ffmpegReason) {
|
|
9166
9246
|
const total = failures.reduce((sum, failure) => sum + failure.count, 0);
|
|
9167
9247
|
const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
|
|
9168
|
-
|
|
9248
|
+
const reason = ffmpegReason ? `: ffmpeg said: ${ffmpegReason}` : "";
|
|
9249
|
+
super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]${reason}`);
|
|
9169
9250
|
this.code = code;
|
|
9170
9251
|
this.retryable = retryable;
|
|
9171
9252
|
this.failures = failures;
|
|
@@ -9207,7 +9288,8 @@ function buildVideoExtractionStageError(result) {
|
|
|
9207
9288
|
retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE",
|
|
9208
9289
|
retryable,
|
|
9209
9290
|
failures,
|
|
9210
|
-
extractionFailureMetadataFromResult(result)
|
|
9291
|
+
extractionFailureMetadataFromResult(result),
|
|
9292
|
+
result.errors.find((failure) => failure.ffmpegReason)?.ffmpegReason
|
|
9211
9293
|
);
|
|
9212
9294
|
}
|
|
9213
9295
|
function buildHdrProbeStageError(failures) {
|
|
@@ -9448,16 +9530,17 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
9448
9530
|
|
|
9449
9531
|
// src/services/render/stages/audioStage.ts
|
|
9450
9532
|
import { join as join20 } from "node:path";
|
|
9451
|
-
import { rmSync as rmSync10 } from "node:fs";
|
|
9452
9533
|
import {
|
|
9453
9534
|
MIXED_AUDIO_FILENAME,
|
|
9454
9535
|
audioFailureDiagnostics,
|
|
9455
|
-
processCompositionAudio
|
|
9536
|
+
processCompositionAudio,
|
|
9537
|
+
removeScratch as removeScratch2,
|
|
9538
|
+
collectScratchLeftovers
|
|
9456
9539
|
} from "@hyperframes/engine";
|
|
9457
9540
|
|
|
9458
9541
|
// src/services/render/audioPadTrim.ts
|
|
9459
9542
|
import { spawn } from "node:child_process";
|
|
9460
|
-
import { mkdtempSync as mkdtempSync3
|
|
9543
|
+
import { mkdtempSync as mkdtempSync3 } from "node:fs";
|
|
9461
9544
|
import { dirname as dirname8, join as join19 } from "node:path";
|
|
9462
9545
|
import {
|
|
9463
9546
|
DEFAULT_CONFIG,
|
|
@@ -9466,6 +9549,7 @@ import {
|
|
|
9466
9549
|
extractAudioMetadata as extractAudioMetadata3,
|
|
9467
9550
|
getFfprobeBinary,
|
|
9468
9551
|
ManagedChildProcess,
|
|
9552
|
+
removeScratch,
|
|
9469
9553
|
renameWithRetry as renameWithRetry5,
|
|
9470
9554
|
runFfmpeg as runFfmpeg5,
|
|
9471
9555
|
trackChildProcess
|
|
@@ -9661,7 +9745,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
9661
9745
|
error: `audioPadTrim: failed to materialize ${plan.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
9662
9746
|
};
|
|
9663
9747
|
} finally {
|
|
9664
|
-
for (const path of plan.cleanupPaths)
|
|
9748
|
+
for (const path of plan.cleanupPaths) removeScratch(path);
|
|
9665
9749
|
}
|
|
9666
9750
|
let loweredDb = 0;
|
|
9667
9751
|
if (probeTruePeak) {
|
|
@@ -9724,7 +9808,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9724
9808
|
}
|
|
9725
9809
|
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
9726
9810
|
if (measuredPath !== correctedPath) return { success: true };
|
|
9727
|
-
renameWithRetry5(correctedPath, input.audioPath);
|
|
9811
|
+
renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
|
|
9728
9812
|
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
9729
9813
|
}
|
|
9730
9814
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
@@ -9765,7 +9849,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9765
9849
|
)}`
|
|
9766
9850
|
};
|
|
9767
9851
|
} finally {
|
|
9768
|
-
if (scratchDir)
|
|
9852
|
+
if (scratchDir) removeScratch(scratchDir);
|
|
9769
9853
|
}
|
|
9770
9854
|
}
|
|
9771
9855
|
function failResult(outputPath, target, source, error) {
|
|
@@ -9910,7 +9994,15 @@ function assertAudioNotInterrupted(failures) {
|
|
|
9910
9994
|
])
|
|
9911
9995
|
});
|
|
9912
9996
|
}
|
|
9913
|
-
|
|
9997
|
+
function runAudioStage(input) {
|
|
9998
|
+
return collectScratchLeftovers(
|
|
9999
|
+
() => runAudioStageScoped(input),
|
|
10000
|
+
(left) => {
|
|
10001
|
+
if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
|
|
10002
|
+
}
|
|
10003
|
+
);
|
|
10004
|
+
}
|
|
10005
|
+
async function runAudioStageScoped(input) {
|
|
9914
10006
|
const stageStart = Date.now();
|
|
9915
10007
|
const pcmSourcePath = join20(input.workDir, "audio-source.wav");
|
|
9916
10008
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
@@ -9932,7 +10024,7 @@ async function runAudioStage(input) {
|
|
|
9932
10024
|
}
|
|
9933
10025
|
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
9934
10026
|
} finally {
|
|
9935
|
-
|
|
10027
|
+
removeScratch2(pcmSourcePath);
|
|
9936
10028
|
}
|
|
9937
10029
|
}
|
|
9938
10030
|
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
@@ -10194,6 +10286,7 @@ async function runCaptureStage(input) {
|
|
|
10194
10286
|
dedupPerfs,
|
|
10195
10287
|
pageErrorLines: workerPageErrors,
|
|
10196
10288
|
openAttempt: input.openAttempt,
|
|
10289
|
+
verifyFirstSession: input.verifyFirstSession,
|
|
10197
10290
|
onProgress: (progress) => {
|
|
10198
10291
|
if (progress.latestWorkerPhase) {
|
|
10199
10292
|
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
@@ -10412,7 +10505,7 @@ import {
|
|
|
10412
10505
|
initializeSession as initializeSession7,
|
|
10413
10506
|
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
10414
10507
|
} from "@hyperframes/engine";
|
|
10415
|
-
import { fpsToNumber as
|
|
10508
|
+
import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
|
|
10416
10509
|
|
|
10417
10510
|
// src/services/hdrImageTransferCache.ts
|
|
10418
10511
|
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
@@ -11086,6 +11179,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11086
11179
|
preset,
|
|
11087
11180
|
effectiveQuality,
|
|
11088
11181
|
effectiveBitrate,
|
|
11182
|
+
deliveryEncoder,
|
|
11089
11183
|
fileServer,
|
|
11090
11184
|
buildCaptureOptions,
|
|
11091
11185
|
createRenderVideoFrameInjector,
|
|
@@ -11147,7 +11241,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11147
11241
|
}
|
|
11148
11242
|
return map;
|
|
11149
11243
|
});
|
|
11150
|
-
const fpsDecimal =
|
|
11244
|
+
const fpsDecimal = fpsToNumber7(job.config.fps);
|
|
11151
11245
|
const transitionRanges = transitionMeta.map((t) => ({
|
|
11152
11246
|
...t,
|
|
11153
11247
|
startFrame: Math.floor(t.time * fpsDecimal),
|
|
@@ -11175,6 +11269,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11175
11269
|
preset: preset.preset,
|
|
11176
11270
|
quality: effectiveQuality,
|
|
11177
11271
|
bitrate: effectiveBitrate,
|
|
11272
|
+
...deliveryEncoder,
|
|
11178
11273
|
pixelFormat: preset.pixelFormat,
|
|
11179
11274
|
hdr: preset.hdr,
|
|
11180
11275
|
rawInputFormat: "rgb48le"
|
|
@@ -11234,7 +11329,7 @@ async function runCaptureHdrStage(input) {
|
|
|
11234
11329
|
beforeCaptureHook: domSession.onBeforeCapture,
|
|
11235
11330
|
width,
|
|
11236
11331
|
height,
|
|
11237
|
-
fps:
|
|
11332
|
+
fps: fpsToNumber7(job.config.fps),
|
|
11238
11333
|
compositeTransfer,
|
|
11239
11334
|
nativeHdrImageIds,
|
|
11240
11335
|
hdrImageBuffers,
|
|
@@ -11378,7 +11473,7 @@ import {
|
|
|
11378
11473
|
mkdirSync as mkdirSync9,
|
|
11379
11474
|
readdirSync as readdirSync4,
|
|
11380
11475
|
readFileSync as readFileSync8,
|
|
11381
|
-
rmSync as
|
|
11476
|
+
rmSync as rmSync9,
|
|
11382
11477
|
statSync as statSync7,
|
|
11383
11478
|
writeFileSync as writeFileSync5
|
|
11384
11479
|
} from "node:fs";
|
|
@@ -11517,7 +11612,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
11517
11612
|
fileSize
|
|
11518
11613
|
};
|
|
11519
11614
|
} finally {
|
|
11520
|
-
|
|
11615
|
+
rmSync9(input.palettePath, { force: true });
|
|
11521
11616
|
}
|
|
11522
11617
|
}
|
|
11523
11618
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -11541,6 +11636,8 @@ function videoEncoderOptions(input, engineCfg) {
|
|
|
11541
11636
|
preset: preset.preset,
|
|
11542
11637
|
quality: input.effectiveQuality,
|
|
11543
11638
|
bitrate: input.effectiveBitrate,
|
|
11639
|
+
maxBitrate: input.maxBitrate,
|
|
11640
|
+
bufferSize: input.bufferSize,
|
|
11544
11641
|
pixelFormat: preset.pixelFormat,
|
|
11545
11642
|
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
11546
11643
|
useGpu: job.config.useGpu,
|
|
@@ -12249,7 +12346,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
12249
12346
|
});
|
|
12250
12347
|
} : void 0,
|
|
12251
12348
|
void 0,
|
|
12252
|
-
options.cfg
|
|
12349
|
+
options.cfg,
|
|
12350
|
+
{ verifyFirstSession: options.verifyFirstSession }
|
|
12253
12351
|
);
|
|
12254
12352
|
pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
|
|
12255
12353
|
if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
|
|
@@ -12815,6 +12913,7 @@ function resolveRenderDebugDir() {
|
|
|
12815
12913
|
}
|
|
12816
12914
|
async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
|
|
12817
12915
|
validateHlsRenderConfig(job.config);
|
|
12916
|
+
validateDeliveryEncoderConfig(job.config);
|
|
12818
12917
|
const debugDir = resolveRenderDebugDir();
|
|
12819
12918
|
const outputDir = dirname11(outputPath);
|
|
12820
12919
|
if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
|
|
@@ -12835,7 +12934,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
12835
12934
|
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
12836
12935
|
return;
|
|
12837
12936
|
}
|
|
12838
|
-
|
|
12937
|
+
rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
12839
12938
|
});
|
|
12840
12939
|
try {
|
|
12841
12940
|
await executeRenderPipeline({
|
|
@@ -12893,11 +12992,14 @@ async function executeRenderPipeline(input) {
|
|
|
12893
12992
|
const isGif = outputFormat === "gif";
|
|
12894
12993
|
const isHls = outputFormat === "hls";
|
|
12895
12994
|
const hlsSegmentSeconds = isHls ? resolveHlsSegmentSeconds(job.config) : void 0;
|
|
12896
|
-
const
|
|
12897
|
-
outputFormat,
|
|
12898
|
-
job.config.fps,
|
|
12899
|
-
job.config.hlsSegmentSeconds
|
|
12900
|
-
|
|
12995
|
+
const deliveryEncoder = resolveDeliveryEncoderOptions({
|
|
12996
|
+
format: outputFormat,
|
|
12997
|
+
fps: job.config.fps,
|
|
12998
|
+
hlsSegmentSeconds: job.config.hlsSegmentSeconds,
|
|
12999
|
+
keyframeIntervalSeconds: job.config.keyframeIntervalSeconds,
|
|
13000
|
+
maxBitrate: job.config.maxBitrate,
|
|
13001
|
+
bufferSize: job.config.bufferSize
|
|
13002
|
+
});
|
|
12901
13003
|
const artifactTransaction = new ArtifactTransaction(
|
|
12902
13004
|
outputPath,
|
|
12903
13005
|
isPngSequence || isHls ? "directory" : "file",
|
|
@@ -13137,7 +13239,7 @@ async function executeRenderPipeline(input) {
|
|
|
13137
13239
|
validateRenderDuration({
|
|
13138
13240
|
duration: probeResult.duration,
|
|
13139
13241
|
totalFrames,
|
|
13140
|
-
fps:
|
|
13242
|
+
fps: fpsToNumber8(job.config.fps)
|
|
13141
13243
|
});
|
|
13142
13244
|
perfStages.browserProbeMs = probeResult.browserProbeMs;
|
|
13143
13245
|
perfStages.compileMs = Date.now() - stage1Start;
|
|
@@ -13980,6 +14082,7 @@ async function executeRenderPipeline(input) {
|
|
|
13980
14082
|
preset,
|
|
13981
14083
|
effectiveQuality,
|
|
13982
14084
|
effectiveBitrate,
|
|
14085
|
+
deliveryEncoder,
|
|
13983
14086
|
fileServer: activeFileServer,
|
|
13984
14087
|
buildCaptureOptions,
|
|
13985
14088
|
createRenderVideoFrameInjector,
|
|
@@ -14004,9 +14107,7 @@ async function executeRenderPipeline(input) {
|
|
|
14004
14107
|
} else {
|
|
14005
14108
|
let streamingHandled = false;
|
|
14006
14109
|
if (capturePlan.kind === "sdr_segmented") {
|
|
14007
|
-
const segmentedPlan = capturePlan;
|
|
14008
14110
|
const captureFrameStart = Date.now();
|
|
14009
|
-
resetCaptureAttemptProgress(job);
|
|
14010
14111
|
const segmentFrames = resolveSegmentFrames(process.env);
|
|
14011
14112
|
const segmentImageFormat = captureOptions.format || "jpeg";
|
|
14012
14113
|
const segmentPlanHash = computeSegmentPlanHash({
|
|
@@ -14021,6 +14122,9 @@ async function executeRenderPipeline(input) {
|
|
|
14021
14122
|
preset: preset.preset,
|
|
14022
14123
|
quality: effectiveQuality,
|
|
14023
14124
|
bitrate: effectiveBitrate,
|
|
14125
|
+
maxBitrate: deliveryEncoder.maxBitrate,
|
|
14126
|
+
bufferSize: deliveryEncoder.bufferSize,
|
|
14127
|
+
gopSize: deliveryEncoder.gopSize,
|
|
14024
14128
|
pixelFormat: preset.pixelFormat,
|
|
14025
14129
|
imageFormat: segmentImageFormat,
|
|
14026
14130
|
useGpu: job.config.useGpu === true,
|
|
@@ -14046,71 +14150,129 @@ async function executeRenderPipeline(input) {
|
|
|
14046
14150
|
});
|
|
14047
14151
|
}
|
|
14048
14152
|
if (completedSegments.size === 0) {
|
|
14049
|
-
|
|
14153
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14050
14154
|
}
|
|
14051
14155
|
} else {
|
|
14052
|
-
|
|
14156
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14053
14157
|
}
|
|
14054
|
-
const
|
|
14158
|
+
const emptySegmentManifest = () => ({
|
|
14055
14159
|
version: 1,
|
|
14056
14160
|
planHash: segmentPlanHash,
|
|
14057
14161
|
totalFrames,
|
|
14058
14162
|
segmentFrames,
|
|
14059
14163
|
completed: []
|
|
14164
|
+
});
|
|
14165
|
+
let segmentManifest = readSegmentManifest(segmentDir) ?? emptySegmentManifest();
|
|
14166
|
+
const invokeSegmented = () => {
|
|
14167
|
+
if (capturePlan.kind !== "sdr_segmented") {
|
|
14168
|
+
throw new Error(`Cannot invoke segmented stage with ${capturePlan.kind} plan`);
|
|
14169
|
+
}
|
|
14170
|
+
const segmentedPlan = capturePlan;
|
|
14171
|
+
resetCaptureAttemptProgress(job);
|
|
14172
|
+
return observeRenderStage(
|
|
14173
|
+
observability,
|
|
14174
|
+
"capture_segmented",
|
|
14175
|
+
captureStageObservationData(),
|
|
14176
|
+
() => runCaptureSegmentedStage({
|
|
14177
|
+
fileServer: activeFileServer,
|
|
14178
|
+
workDir,
|
|
14179
|
+
framesDir,
|
|
14180
|
+
videoOnlyPath,
|
|
14181
|
+
job,
|
|
14182
|
+
totalFrames,
|
|
14183
|
+
cfg,
|
|
14184
|
+
plan: segmentedPlan,
|
|
14185
|
+
log,
|
|
14186
|
+
probeSession,
|
|
14187
|
+
outputFormat,
|
|
14188
|
+
streamingEncoderOptions: {
|
|
14189
|
+
fps: job.config.fps,
|
|
14190
|
+
width: captureCompositionWidth ?? width,
|
|
14191
|
+
height: captureCompositionHeight ?? height,
|
|
14192
|
+
codec: preset.codec,
|
|
14193
|
+
preset: preset.preset,
|
|
14194
|
+
quality: effectiveQuality,
|
|
14195
|
+
bitrate: effectiveBitrate,
|
|
14196
|
+
pixelFormat: preset.pixelFormat,
|
|
14197
|
+
vp9CpuUsed: cfg.vp9CpuUsed,
|
|
14198
|
+
useGpu: job.config.useGpu,
|
|
14199
|
+
imageFormat: segmentImageFormat,
|
|
14200
|
+
hdr: preset.hdr,
|
|
14201
|
+
// Each segment sets its own GOP (at most the interval); hls never reaches this route.
|
|
14202
|
+
...deliveryEncoder
|
|
14203
|
+
},
|
|
14204
|
+
buildCaptureOptions,
|
|
14205
|
+
createRenderVideoFrameInjector,
|
|
14206
|
+
abortSignal: executionSignal,
|
|
14207
|
+
assertNotAborted,
|
|
14208
|
+
onProgress,
|
|
14209
|
+
dedupPerfs,
|
|
14210
|
+
segmentFrames,
|
|
14211
|
+
segmentDir,
|
|
14212
|
+
workerCount: segmentedPlan.workerCount,
|
|
14213
|
+
browserRecycleEverySegments: resolveSegmentBrowserRecycle(process.env),
|
|
14214
|
+
completedSegments,
|
|
14215
|
+
onSegmentComplete: (entry) => {
|
|
14216
|
+
segmentManifest.completed = [
|
|
14217
|
+
...segmentManifest.completed.filter((e) => e.index !== entry.index),
|
|
14218
|
+
{ ...entry, completedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
14219
|
+
];
|
|
14220
|
+
writeSegmentManifest(segmentDir, segmentManifest);
|
|
14221
|
+
},
|
|
14222
|
+
updateCaptureObservability
|
|
14223
|
+
})
|
|
14224
|
+
);
|
|
14060
14225
|
};
|
|
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
|
-
);
|
|
14226
|
+
let segmentedRes;
|
|
14227
|
+
try {
|
|
14228
|
+
segmentedRes = await invokeSegmented();
|
|
14229
|
+
} catch (err) {
|
|
14230
|
+
const isVerifyError = isDrawElementVerificationError(err);
|
|
14231
|
+
const isDeStall = isDeRendererStallError(err);
|
|
14232
|
+
if (!isVerifyError && !isDrawElementCaptureError(err) && !isDeStall || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
|
|
14233
|
+
throw err;
|
|
14234
|
+
}
|
|
14235
|
+
deSelfVerifyFallback = isVerifyError;
|
|
14236
|
+
const t = deVerifyFallbackTelemetry(err);
|
|
14237
|
+
deFallbackReason = isVerifyError ? t.reason : isDeStall ? "de_renderer_stall" : "capture_error";
|
|
14238
|
+
deFallbackFailedDb = t.failedDb;
|
|
14239
|
+
deFallbackFrameIndex = t.frameIndex;
|
|
14240
|
+
deFallbackThresholdDb = t.thresholdDb;
|
|
14241
|
+
log.warn(
|
|
14242
|
+
"[Render] drawElement capture failed on the segmented path; re-rendering via screenshot",
|
|
14243
|
+
{
|
|
14244
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14245
|
+
}
|
|
14246
|
+
);
|
|
14247
|
+
observability.checkpoint(
|
|
14248
|
+
"capture_segmented",
|
|
14249
|
+
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
14250
|
+
);
|
|
14251
|
+
if (probeSession) {
|
|
14252
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
14253
|
+
const orphaned = probeSession;
|
|
14254
|
+
probeSession = null;
|
|
14255
|
+
await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
|
|
14256
|
+
}
|
|
14257
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14258
|
+
completedSegments = /* @__PURE__ */ new Set();
|
|
14259
|
+
segmentManifest = emptySegmentManifest();
|
|
14260
|
+
dedupPerfs.length = 0;
|
|
14261
|
+
capturePlan = replanAfterFailure(capturePlan, {
|
|
14262
|
+
kind: isVerifyError ? "draw_element_verification" : "draw_element_capture"
|
|
14263
|
+
});
|
|
14264
|
+
syncCapturePlan();
|
|
14265
|
+
updateCaptureObservability({
|
|
14266
|
+
forceScreenshot: capturePlan.forceScreenshot,
|
|
14267
|
+
deSelfVerifyFallback,
|
|
14268
|
+
deFallbackReason,
|
|
14269
|
+
deFallbackFailedDb,
|
|
14270
|
+
deFallbackFrameIndex,
|
|
14271
|
+
deFallbackThresholdDb
|
|
14272
|
+
});
|
|
14273
|
+
segmentedRes = await invokeSegmented();
|
|
14274
|
+
observability.clearFailure("capture_segmented");
|
|
14275
|
+
}
|
|
14114
14276
|
if (segmentedRes.success) {
|
|
14115
14277
|
streamingHandled = true;
|
|
14116
14278
|
workerCount = segmentedRes.workerCount;
|
|
@@ -14130,7 +14292,7 @@ async function executeRenderPipeline(input) {
|
|
|
14130
14292
|
);
|
|
14131
14293
|
updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
|
|
14132
14294
|
if (job.config.keepSegments !== true) {
|
|
14133
|
-
|
|
14295
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14134
14296
|
}
|
|
14135
14297
|
} else {
|
|
14136
14298
|
capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
|
|
@@ -14183,7 +14345,7 @@ async function executeRenderPipeline(input) {
|
|
|
14183
14345
|
// assemble stage's `-c copy` segmentation cuts exactly on
|
|
14184
14346
|
// the segment boundary. Every other format leaves the
|
|
14185
14347
|
// encoder's open-GOP output untouched.
|
|
14186
|
-
...
|
|
14348
|
+
...deliveryEncoder
|
|
14187
14349
|
},
|
|
14188
14350
|
buildCaptureOptions,
|
|
14189
14351
|
createRenderVideoFrameInjector,
|
|
@@ -14346,7 +14508,7 @@ async function executeRenderPipeline(input) {
|
|
|
14346
14508
|
engineConfig: cfg,
|
|
14347
14509
|
// Same value the streaming encoder above spreads — the disk and
|
|
14348
14510
|
// chunked-concat encoders need an identical GOP lock for HLS.
|
|
14349
|
-
...
|
|
14511
|
+
...deliveryEncoder,
|
|
14350
14512
|
abortSignal: executionSignal,
|
|
14351
14513
|
assertNotAborted,
|
|
14352
14514
|
onProgress
|
|
@@ -14402,7 +14564,7 @@ async function executeRenderPipeline(input) {
|
|
|
14402
14564
|
"capture_disk",
|
|
14403
14565
|
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
14404
14566
|
);
|
|
14405
|
-
|
|
14567
|
+
rmSync10(framesDir, { recursive: true, force: true });
|
|
14406
14568
|
mkdirSync10(framesDir, { recursive: true });
|
|
14407
14569
|
resetCaptureAttemptProgress(job);
|
|
14408
14570
|
dedupPerfs.length = 0;
|
|
@@ -14500,7 +14662,7 @@ async function executeRenderPipeline(input) {
|
|
|
14500
14662
|
buildArtifactExpectation({
|
|
14501
14663
|
outputFormat,
|
|
14502
14664
|
durationSeconds: job.duration,
|
|
14503
|
-
fps:
|
|
14665
|
+
fps: fpsToNumber8(job.config.fps),
|
|
14504
14666
|
expectedFrames: captureTotalFrames,
|
|
14505
14667
|
width: outputWidth,
|
|
14506
14668
|
height: outputHeight
|
|
@@ -14930,11 +15092,12 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
|
14930
15092
|
import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
|
|
14931
15093
|
import { extname as extname4, isAbsolute as isAbsolute4, join as join30, relative as relative4, sep as sep3 } from "node:path";
|
|
14932
15094
|
import {
|
|
14933
|
-
fpsToNumber as
|
|
15095
|
+
fpsToNumber as fpsToNumber9,
|
|
14934
15096
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
14935
15097
|
} from "@hyperframes/core";
|
|
14936
15098
|
import {
|
|
14937
15099
|
getEncoderPreset as getEncoderPreset4,
|
|
15100
|
+
keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
|
|
14938
15101
|
normalizeVp9CpuUsed,
|
|
14939
15102
|
resolveConfig as resolveConfig3,
|
|
14940
15103
|
declareContentAddressedUrl,
|
|
@@ -15130,6 +15293,12 @@ function assertOptionalInteger(options, field, min = 0) {
|
|
|
15130
15293
|
throw new Error(`Render request ${field} must be an integer >= ${min}`);
|
|
15131
15294
|
}
|
|
15132
15295
|
}
|
|
15296
|
+
function assertOptionalPositiveNumber(options, field) {
|
|
15297
|
+
const value = options[field];
|
|
15298
|
+
if (value !== void 0 && !(typeof value === "number" && value > 0)) {
|
|
15299
|
+
throw new Error(`Render request ${field} must be a positive number`);
|
|
15300
|
+
}
|
|
15301
|
+
}
|
|
15133
15302
|
function assertOptionalString(options, field) {
|
|
15134
15303
|
if (options[field] !== void 0 && typeof options[field] !== "string") {
|
|
15135
15304
|
throw new Error(`Render request ${field} must be a string`);
|
|
@@ -15177,10 +15346,11 @@ function assertRequestOptionScalars(options) {
|
|
|
15177
15346
|
assertOptionalInteger(options, "workers", 1);
|
|
15178
15347
|
assertOptionalInteger(options, "maxRenderMs");
|
|
15179
15348
|
assertOptionalInteger(options, "crf");
|
|
15349
|
+
assertOptionalPositiveNumber(options, "keyframeIntervalSeconds");
|
|
15180
15350
|
for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
|
|
15181
15351
|
assertOptionalBoolean(options, field);
|
|
15182
15352
|
}
|
|
15183
|
-
for (const field of ["entryFile", "videoBitrate"]) {
|
|
15353
|
+
for (const field of ["entryFile", "videoBitrate", "maxBitrate", "bufferSize"]) {
|
|
15184
15354
|
assertOptionalString(options, field);
|
|
15185
15355
|
}
|
|
15186
15356
|
assertOptionalEnum(options, "strictness", ["strict", "best-effort"]);
|
|
@@ -15341,7 +15511,7 @@ function parseRenderOptions(body) {
|
|
|
15341
15511
|
const format = parseServerFormat(body.format);
|
|
15342
15512
|
const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
|
|
15343
15513
|
const encoder = parseEncoderOptions(body, format ?? "mp4");
|
|
15344
|
-
const { crf, videoBitrate, videoFrameFormat } = encoder.ok ? encoder.options : {};
|
|
15514
|
+
const { crf, videoBitrate, videoFrameFormat, keyframeIntervalSeconds, maxBitrate, bufferSize } = encoder.ok ? encoder.options : {};
|
|
15345
15515
|
const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
|
|
15346
15516
|
return {
|
|
15347
15517
|
outputPath,
|
|
@@ -15351,6 +15521,9 @@ function parseRenderOptions(body) {
|
|
|
15351
15521
|
maxRenderMs,
|
|
15352
15522
|
crf,
|
|
15353
15523
|
videoBitrate,
|
|
15524
|
+
keyframeIntervalSeconds,
|
|
15525
|
+
maxBitrate,
|
|
15526
|
+
bufferSize,
|
|
15354
15527
|
useGpu,
|
|
15355
15528
|
debug,
|
|
15356
15529
|
strictness,
|
|
@@ -15385,6 +15558,9 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
15385
15558
|
maxRenderMs: input.maxRenderMs,
|
|
15386
15559
|
crf: input.crf,
|
|
15387
15560
|
videoBitrate: input.videoBitrate,
|
|
15561
|
+
keyframeIntervalSeconds: input.keyframeIntervalSeconds,
|
|
15562
|
+
maxBitrate: input.maxBitrate,
|
|
15563
|
+
bufferSize: input.bufferSize,
|
|
15388
15564
|
useGpu: input.useGpu,
|
|
15389
15565
|
debug: input.debug,
|
|
15390
15566
|
strictness: input.strictness,
|
|
@@ -15531,7 +15707,7 @@ function createArtifactStore(ttlMs) {
|
|
|
15531
15707
|
function cleanupTempDir(dir, log) {
|
|
15532
15708
|
if (!dir) return;
|
|
15533
15709
|
try {
|
|
15534
|
-
|
|
15710
|
+
rmSync11(dir, { recursive: true, force: true });
|
|
15535
15711
|
} catch (error) {
|
|
15536
15712
|
log.warn("Failed to cleanup temp project dir", {
|
|
15537
15713
|
cleanupProjectDir: dir,
|