@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/distributed.js
CHANGED
|
@@ -6,16 +6,17 @@ import {
|
|
|
6
6
|
existsSync as existsSync15,
|
|
7
7
|
linkSync,
|
|
8
8
|
mkdirSync as mkdirSync10,
|
|
9
|
-
rmSync as
|
|
9
|
+
rmSync as rmSync7,
|
|
10
10
|
writeFileSync as writeFileSync7
|
|
11
11
|
} from "node:fs";
|
|
12
12
|
import { extname as extname4, isAbsolute as isAbsolute3, join as join22, relative as relative5, sep as sep2 } from "node:path";
|
|
13
13
|
import {
|
|
14
|
-
fpsToNumber as
|
|
14
|
+
fpsToNumber as fpsToNumber9,
|
|
15
15
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
16
16
|
} from "@hyperframes/core";
|
|
17
17
|
import {
|
|
18
18
|
getEncoderPreset as getEncoderPreset4,
|
|
19
|
+
keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
|
|
19
20
|
normalizeVp9CpuUsed,
|
|
20
21
|
resolveConfig as resolveConfig3,
|
|
21
22
|
declareContentAddressedUrl,
|
|
@@ -69,7 +70,7 @@ import {
|
|
|
69
70
|
mkdirSync as mkdirSync6,
|
|
70
71
|
readFileSync as readFileSync6,
|
|
71
72
|
readdirSync as readdirSync3,
|
|
72
|
-
rmSync as
|
|
73
|
+
rmSync as rmSync5,
|
|
73
74
|
statSync as statSync5,
|
|
74
75
|
writeFileSync as writeFileSync4,
|
|
75
76
|
copyFileSync as copyFileSync5,
|
|
@@ -77,7 +78,7 @@ import {
|
|
|
77
78
|
} from "fs";
|
|
78
79
|
import {
|
|
79
80
|
chainCapture,
|
|
80
|
-
fpsToNumber as
|
|
81
|
+
fpsToNumber as fpsToNumber8,
|
|
81
82
|
HF_COLOR_GRADING_ATTR,
|
|
82
83
|
HF_VFX_ATTR,
|
|
83
84
|
parseVfxChain,
|
|
@@ -822,7 +823,34 @@ function outputNeedsAlpha(format) {
|
|
|
822
823
|
}
|
|
823
824
|
|
|
824
825
|
// src/services/render/hlsConfig.ts
|
|
825
|
-
import {
|
|
826
|
+
import { keyframeIntervalToGopSize } from "@hyperframes/engine";
|
|
827
|
+
|
|
828
|
+
// src/services/render/deliveryEncoder.ts
|
|
829
|
+
import { appendRateCapArgs, keyframeIntervalToGopSize as keyframeIntervalToGopSize2 } from "@hyperframes/engine";
|
|
830
|
+
function validateDeliveryEncoderConfig(config) {
|
|
831
|
+
const { keyframeIntervalSeconds, maxBitrate, bufferSize, format } = config;
|
|
832
|
+
if (keyframeIntervalSeconds === void 0 && maxBitrate === void 0 && bufferSize === void 0)
|
|
833
|
+
return;
|
|
834
|
+
if (keyframeIntervalSeconds !== void 0) {
|
|
835
|
+
if (!Number.isFinite(keyframeIntervalSeconds) || keyframeIntervalSeconds <= 0) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`keyframeIntervalSeconds must be a positive number of seconds; got ${keyframeIntervalSeconds}`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
if (format === "hls") {
|
|
841
|
+
throw new Error(
|
|
842
|
+
"keyframeIntervalSeconds does not apply to hls; hlsSegmentSeconds sets its keyframes"
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (format !== void 0 && format !== "mp4") {
|
|
847
|
+
throw new Error(`keyframeIntervalSeconds and maxBitrate apply to mp4 renders, not ${format}`);
|
|
848
|
+
}
|
|
849
|
+
appendRateCapArgs([], { maxBitrate, bufferSize });
|
|
850
|
+
}
|
|
851
|
+
function chunkGopFrames(framesInChunk, intervalFrames) {
|
|
852
|
+
return Math.min(framesInChunk, intervalFrames ?? framesInChunk);
|
|
853
|
+
}
|
|
826
854
|
|
|
827
855
|
// src/services/render/shared.ts
|
|
828
856
|
import {
|
|
@@ -1245,7 +1273,7 @@ import {
|
|
|
1245
1273
|
import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
|
|
1246
1274
|
|
|
1247
1275
|
// src/services/render/perfSummary.ts
|
|
1248
|
-
import { fpsToNumber as
|
|
1276
|
+
import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
|
|
1249
1277
|
function pushWorkerDedupPerfs(results, sink) {
|
|
1250
1278
|
for (const r of results) {
|
|
1251
1279
|
if (r.perf) sink.push(r.perf);
|
|
@@ -1324,7 +1352,7 @@ import {
|
|
|
1324
1352
|
resolveVideoExtractionWindow,
|
|
1325
1353
|
runFfmpeg
|
|
1326
1354
|
} from "@hyperframes/engine";
|
|
1327
|
-
import { fpsToFfmpegArg, fpsToNumber as
|
|
1355
|
+
import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
|
|
1328
1356
|
var BYTES_PER_MIB = 1024 * 1024;
|
|
1329
1357
|
var DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
|
|
1330
1358
|
|
|
@@ -1375,7 +1403,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
|
1375
1403
|
}
|
|
1376
1404
|
|
|
1377
1405
|
// src/services/render/captureCost.ts
|
|
1378
|
-
import { fpsToNumber as
|
|
1406
|
+
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
1379
1407
|
import {
|
|
1380
1408
|
calculateOptimalWorkers,
|
|
1381
1409
|
captureFrameToBuffer as captureFrameToBuffer3,
|
|
@@ -1785,7 +1813,7 @@ async function observeRenderStage(recorder, phase, data, fn, options = {}) {
|
|
|
1785
1813
|
// src/services/render/videoFrameCoverage.ts
|
|
1786
1814
|
import { parseHTML } from "linkedom";
|
|
1787
1815
|
import {
|
|
1788
|
-
fpsToNumber as
|
|
1816
|
+
fpsToNumber as fpsToNumber5,
|
|
1789
1817
|
normalizeRateSpec,
|
|
1790
1818
|
sourceTimeAt,
|
|
1791
1819
|
toFps
|
|
@@ -2993,7 +3021,7 @@ function detectShaderTransitionUsage(html) {
|
|
|
2993
3021
|
}
|
|
2994
3022
|
return false;
|
|
2995
3023
|
}
|
|
2996
|
-
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
|
|
3024
|
+
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
|
|
2997
3025
|
let filePath = src;
|
|
2998
3026
|
if (isHttpUrl(src)) {
|
|
2999
3027
|
if (!existsSync6(downloadDir)) mkdirSync4(downloadDir, { recursive: true });
|
|
@@ -3020,45 +3048,52 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
3020
3048
|
if (error instanceof Error && error.stack) wrapped.stack = error.stack;
|
|
3021
3049
|
return wrapped;
|
|
3022
3050
|
};
|
|
3023
|
-
|
|
3024
|
-
let profile;
|
|
3051
|
+
const probe = () => withMediaProbeSlot(async () => {
|
|
3025
3052
|
try {
|
|
3026
3053
|
await assertMediaPayload(filePath, elementIdentity);
|
|
3027
|
-
profile = await probeMediaProfile2(filePath);
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
if (error instanceof NotMediaPayloadError2) {
|
|
3031
|
-
log?.warn(
|
|
3032
|
-
`[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.`
|
|
3033
|
-
);
|
|
3034
|
-
}
|
|
3035
|
-
return { duration: null, resolvedPath: filePath };
|
|
3054
|
+
const profile = await probeMediaProfile2(filePath);
|
|
3055
|
+
if (tagName === "video") {
|
|
3056
|
+
return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
|
|
3036
3057
|
}
|
|
3037
|
-
throw withSrcContext(error);
|
|
3038
|
-
}
|
|
3039
|
-
assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);
|
|
3040
|
-
let metadata;
|
|
3041
|
-
if (tagName === "video") {
|
|
3042
|
-
try {
|
|
3043
|
-
metadata = await extractMediaMetadata(filePath);
|
|
3044
|
-
} catch (error) {
|
|
3045
|
-
throw withSrcContext(error);
|
|
3046
|
-
}
|
|
3047
|
-
} else {
|
|
3048
3058
|
try {
|
|
3049
|
-
|
|
3059
|
+
return { profile, fileDuration: (await extractAudioMetadata(filePath)).durationSeconds };
|
|
3050
3060
|
} catch {
|
|
3051
|
-
return {
|
|
3061
|
+
return { profile, fileDuration: null };
|
|
3052
3062
|
}
|
|
3063
|
+
} catch (error) {
|
|
3064
|
+
return { error };
|
|
3053
3065
|
}
|
|
3054
|
-
const fileDuration = metadata.durationSeconds;
|
|
3055
|
-
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
3056
|
-
fileDuration,
|
|
3057
|
-
mediaStart,
|
|
3058
|
-
playbackRate
|
|
3059
|
-
);
|
|
3060
|
-
return { duration, resolvedPath: filePath };
|
|
3061
3066
|
});
|
|
3067
|
+
const key = `${tagName}\0${filePath}`;
|
|
3068
|
+
let probed = probes?.get(key);
|
|
3069
|
+
if (!probed) {
|
|
3070
|
+
probed = probe();
|
|
3071
|
+
probes?.set(key, probed);
|
|
3072
|
+
}
|
|
3073
|
+
const outcome = await probed;
|
|
3074
|
+
if ("error" in outcome) {
|
|
3075
|
+
if (tagName !== "video") {
|
|
3076
|
+
if (outcome.error instanceof NotMediaPayloadError2) {
|
|
3077
|
+
log?.warn(
|
|
3078
|
+
`[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.`
|
|
3079
|
+
);
|
|
3080
|
+
}
|
|
3081
|
+
return { duration: null, resolvedPath: filePath };
|
|
3082
|
+
}
|
|
3083
|
+
throw withSrcContext(outcome.error);
|
|
3084
|
+
}
|
|
3085
|
+
assertAssetMediaTypeProfile(
|
|
3086
|
+
tagName === "video" ? "video" : "audio",
|
|
3087
|
+
outcome.profile,
|
|
3088
|
+
elementIdentity
|
|
3089
|
+
);
|
|
3090
|
+
if (outcome.fileDuration === null) return { duration: null, resolvedPath: filePath };
|
|
3091
|
+
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
3092
|
+
outcome.fileDuration,
|
|
3093
|
+
mediaStart,
|
|
3094
|
+
playbackRate
|
|
3095
|
+
);
|
|
3096
|
+
return { duration, resolvedPath: filePath };
|
|
3062
3097
|
}
|
|
3063
3098
|
function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
3064
3099
|
const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
|
|
@@ -3072,7 +3107,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
|
3072
3107
|
}
|
|
3073
3108
|
return changed ? document2.toString() : html;
|
|
3074
3109
|
}
|
|
3075
|
-
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
|
|
3110
|
+
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath, probes) {
|
|
3076
3111
|
const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
|
|
3077
3112
|
const mediaUnresolved = unresolved.filter(
|
|
3078
3113
|
(el) => (el.tagName === "video" || el.tagName === "audio") && el.src
|
|
@@ -3089,7 +3124,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
3089
3124
|
el.tagName,
|
|
3090
3125
|
el.id,
|
|
3091
3126
|
log,
|
|
3092
|
-
urlByAbsentLocalPath
|
|
3127
|
+
urlByAbsentLocalPath,
|
|
3128
|
+
probes
|
|
3093
3129
|
).then(({ duration }) => ({ id: el.id, duration }))
|
|
3094
3130
|
)
|
|
3095
3131
|
);
|
|
@@ -3110,7 +3146,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
3110
3146
|
downloadDir,
|
|
3111
3147
|
el.tagName,
|
|
3112
3148
|
el.id,
|
|
3113
|
-
log
|
|
3149
|
+
log,
|
|
3150
|
+
void 0,
|
|
3151
|
+
probes
|
|
3114
3152
|
);
|
|
3115
3153
|
return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
|
|
3116
3154
|
})
|
|
@@ -3132,7 +3170,7 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
3132
3170
|
compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
|
|
3133
3171
|
return { html: compiledHtml, unresolvedCompositions };
|
|
3134
3172
|
}
|
|
3135
|
-
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
|
|
3173
|
+
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath, probes) {
|
|
3136
3174
|
const subCompositions = /* @__PURE__ */ new Map();
|
|
3137
3175
|
const { document: document2 } = parseHTML5(html);
|
|
3138
3176
|
const compEls = document2.querySelectorAll("[data-composition-src]");
|
|
@@ -3160,14 +3198,16 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
3160
3198
|
dirname5(item.filePath),
|
|
3161
3199
|
downloadDir,
|
|
3162
3200
|
void 0,
|
|
3163
|
-
urlByAbsentLocalPath
|
|
3201
|
+
urlByAbsentLocalPath,
|
|
3202
|
+
probes
|
|
3164
3203
|
);
|
|
3165
3204
|
const nested = await parseSubCompositions(
|
|
3166
3205
|
compiledSub,
|
|
3167
3206
|
projectDir,
|
|
3168
3207
|
downloadDir,
|
|
3169
3208
|
item.nestedVisited,
|
|
3170
|
-
urlByAbsentLocalPath
|
|
3209
|
+
urlByAbsentLocalPath,
|
|
3210
|
+
probes
|
|
3171
3211
|
);
|
|
3172
3212
|
return {
|
|
3173
3213
|
srcPath: item.srcPath,
|
|
@@ -3937,19 +3977,22 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
3937
3977
|
);
|
|
3938
3978
|
const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
|
|
3939
3979
|
assertSubCompositionsUsable(rawHtml, projectDir);
|
|
3980
|
+
const probes = /* @__PURE__ */ new Map();
|
|
3940
3981
|
const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
|
|
3941
3982
|
rawHtml,
|
|
3942
3983
|
projectDir,
|
|
3943
3984
|
downloadDir,
|
|
3944
3985
|
options.log,
|
|
3945
|
-
options.urlByAbsentLocalPath
|
|
3986
|
+
options.urlByAbsentLocalPath,
|
|
3987
|
+
probes
|
|
3946
3988
|
);
|
|
3947
3989
|
const { subCompositions } = await parseSubCompositions(
|
|
3948
3990
|
compiledHtml,
|
|
3949
3991
|
projectDir,
|
|
3950
3992
|
downloadDir,
|
|
3951
3993
|
void 0,
|
|
3952
|
-
options.urlByAbsentLocalPath
|
|
3994
|
+
options.urlByAbsentLocalPath,
|
|
3995
|
+
probes
|
|
3953
3996
|
);
|
|
3954
3997
|
const fullHtml = ensureFullDocument(compiledHtml);
|
|
3955
3998
|
const inlinedHtml = inlineSubCompositions(
|
|
@@ -4052,9 +4095,12 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
4052
4095
|
}
|
|
4053
4096
|
const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
|
|
4054
4097
|
const masterFxChain = parseMasterFxChain(html);
|
|
4098
|
+
const advised = /* @__PURE__ */ new Set();
|
|
4055
4099
|
for (const video of videos) {
|
|
4056
4100
|
if (isHttpUrl(video.src)) continue;
|
|
4057
4101
|
const videoPath = resolve6(projectDir, video.src);
|
|
4102
|
+
if (advised.has(videoPath)) continue;
|
|
4103
|
+
advised.add(videoPath);
|
|
4058
4104
|
const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
|
|
4059
4105
|
(async () => {
|
|
4060
4106
|
const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
|
|
@@ -4605,13 +4651,14 @@ import {
|
|
|
4605
4651
|
} from "@hyperframes/engine";
|
|
4606
4652
|
import {
|
|
4607
4653
|
MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
|
|
4608
|
-
fpsToNumber as
|
|
4654
|
+
fpsToNumber as fpsToNumber6,
|
|
4609
4655
|
normalizeRateSpec as normalizeRateSpec3,
|
|
4610
4656
|
readMediaStart as readMediaStart2,
|
|
4611
4657
|
shiftRateLane as shiftRateLane2,
|
|
4612
4658
|
sourceTimeAt as sourceTimeAt3,
|
|
4613
4659
|
timeAtSourceTime
|
|
4614
4660
|
} from "@hyperframes/core";
|
|
4661
|
+
import { parseHostVariableValues as parseHostVariableValues2, readDeclaredDefaults as readDeclaredDefaults2 } from "@hyperframes/core/compiler";
|
|
4615
4662
|
import { extractMediaSrcMutations } from "@hyperframes/parsers";
|
|
4616
4663
|
|
|
4617
4664
|
// src/services/render/stages/probeFailures.ts
|
|
@@ -4649,8 +4696,15 @@ function hasAutoStartVideos(html) {
|
|
|
4649
4696
|
const { document: document2 } = parseHTML6(html);
|
|
4650
4697
|
return document2.querySelector("video[data-hf-auto-start]") !== null;
|
|
4651
4698
|
}
|
|
4699
|
+
function variableHasValue(element, variableId, variables) {
|
|
4700
|
+
if (variables && Object.hasOwn(variables, variableId)) return true;
|
|
4701
|
+
for (let scope = element.parentElement; scope; scope = scope.parentElement) {
|
|
4702
|
+
if (Object.hasOwn(parseHostVariableValues2(scope), variableId)) return true;
|
|
4703
|
+
if (Object.hasOwn(readDeclaredDefaults2(scope), variableId)) return true;
|
|
4704
|
+
}
|
|
4705
|
+
return Object.hasOwn(readDeclaredDefaults2(element.ownerDocument.documentElement), variableId);
|
|
4706
|
+
}
|
|
4652
4707
|
function hasVariableBoundMedia(html, variables) {
|
|
4653
|
-
if (!variables || Object.keys(variables).length === 0) return false;
|
|
4654
4708
|
const { document: document2 } = parseHTML6(html);
|
|
4655
4709
|
return Array.from(
|
|
4656
4710
|
document2.querySelectorAll(
|
|
@@ -4658,7 +4712,7 @@ function hasVariableBoundMedia(html, variables) {
|
|
|
4658
4712
|
)
|
|
4659
4713
|
).some((element) => {
|
|
4660
4714
|
const variableId = element.getAttribute("data-var-src")?.trim();
|
|
4661
|
-
return Boolean(variableId &&
|
|
4715
|
+
return Boolean(variableId && variableHasValue(element, variableId, variables));
|
|
4662
4716
|
});
|
|
4663
4717
|
}
|
|
4664
4718
|
function authoredMediaStarts(html) {
|
|
@@ -5084,7 +5138,7 @@ async function runProbeStage(input) {
|
|
|
5084
5138
|
session.page,
|
|
5085
5139
|
composition.audios.map((audio) => audio.id),
|
|
5086
5140
|
composition.duration,
|
|
5087
|
-
|
|
5141
|
+
fpsToNumber6(job.config.fps)
|
|
5088
5142
|
);
|
|
5089
5143
|
assertNotAborted();
|
|
5090
5144
|
if (automation.length > 0) {
|
|
@@ -5147,7 +5201,7 @@ async function runProbeStage(input) {
|
|
|
5147
5201
|
}
|
|
5148
5202
|
const browserProbeMs = Date.now() - probeStart;
|
|
5149
5203
|
const duration = composition.duration;
|
|
5150
|
-
const totalFrames = durationToFrameCount(duration,
|
|
5204
|
+
const totalFrames = durationToFrameCount(duration, fpsToNumber6(job.config.fps));
|
|
5151
5205
|
if (duration <= 0) {
|
|
5152
5206
|
const diagnostics = [];
|
|
5153
5207
|
try {
|
|
@@ -5392,10 +5446,11 @@ var VideoExtractionStageError = class extends Error {
|
|
|
5392
5446
|
kind: failure.kind,
|
|
5393
5447
|
affectedElementCount: failure.count
|
|
5394
5448
|
}))
|
|
5395
|
-
)) {
|
|
5449
|
+
), ffmpegReason) {
|
|
5396
5450
|
const total = failures.reduce((sum2, failure) => sum2 + failure.count, 0);
|
|
5397
5451
|
const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
|
|
5398
|
-
|
|
5452
|
+
const reason = ffmpegReason ? `: ffmpeg said: ${ffmpegReason}` : "";
|
|
5453
|
+
super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]${reason}`);
|
|
5399
5454
|
this.code = code;
|
|
5400
5455
|
this.retryable = retryable;
|
|
5401
5456
|
this.failures = failures;
|
|
@@ -5442,7 +5497,8 @@ function buildVideoExtractionStageError(result) {
|
|
|
5442
5497
|
retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE",
|
|
5443
5498
|
retryable,
|
|
5444
5499
|
failures,
|
|
5445
|
-
extractionFailureMetadataFromResult(result)
|
|
5500
|
+
extractionFailureMetadataFromResult(result),
|
|
5501
|
+
result.errors.find((failure) => failure.ffmpegReason)?.ffmpegReason
|
|
5446
5502
|
);
|
|
5447
5503
|
}
|
|
5448
5504
|
function buildHdrProbeStageError(failures) {
|
|
@@ -5710,16 +5766,17 @@ async function appendVideoAudioFromProbe(input) {
|
|
|
5710
5766
|
|
|
5711
5767
|
// src/services/render/stages/audioStage.ts
|
|
5712
5768
|
import { join as join11 } from "node:path";
|
|
5713
|
-
import { rmSync as rmSync5 } from "node:fs";
|
|
5714
5769
|
import {
|
|
5715
5770
|
MIXED_AUDIO_FILENAME,
|
|
5716
5771
|
audioFailureDiagnostics,
|
|
5717
|
-
processCompositionAudio
|
|
5772
|
+
processCompositionAudio,
|
|
5773
|
+
removeScratch as removeScratch2,
|
|
5774
|
+
collectScratchLeftovers
|
|
5718
5775
|
} from "@hyperframes/engine";
|
|
5719
5776
|
|
|
5720
5777
|
// src/services/render/audioPadTrim.ts
|
|
5721
5778
|
import { spawn } from "node:child_process";
|
|
5722
|
-
import { mkdtempSync
|
|
5779
|
+
import { mkdtempSync } from "node:fs";
|
|
5723
5780
|
import { dirname as dirname6, join as join10 } from "node:path";
|
|
5724
5781
|
import {
|
|
5725
5782
|
DEFAULT_CONFIG,
|
|
@@ -5728,6 +5785,7 @@ import {
|
|
|
5728
5785
|
extractAudioMetadata as extractAudioMetadata3,
|
|
5729
5786
|
getFfprobeBinary,
|
|
5730
5787
|
ManagedChildProcess,
|
|
5788
|
+
removeScratch,
|
|
5731
5789
|
renameWithRetry as renameWithRetry5,
|
|
5732
5790
|
runFfmpeg as runFfmpeg5,
|
|
5733
5791
|
trackChildProcess
|
|
@@ -5923,7 +5981,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
5923
5981
|
error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
5924
5982
|
};
|
|
5925
5983
|
} finally {
|
|
5926
|
-
for (const path of plan2.cleanupPaths)
|
|
5984
|
+
for (const path of plan2.cleanupPaths) removeScratch(path);
|
|
5927
5985
|
}
|
|
5928
5986
|
let loweredDb = 0;
|
|
5929
5987
|
if (probeTruePeak) {
|
|
@@ -5986,7 +6044,7 @@ async function enforceAacTruePeak(input) {
|
|
|
5986
6044
|
}
|
|
5987
6045
|
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
5988
6046
|
if (measuredPath !== correctedPath) return { success: true };
|
|
5989
|
-
renameWithRetry5(correctedPath, input.audioPath);
|
|
6047
|
+
renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
|
|
5990
6048
|
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
5991
6049
|
}
|
|
5992
6050
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
@@ -6027,7 +6085,7 @@ async function enforceAacTruePeak(input) {
|
|
|
6027
6085
|
)}`
|
|
6028
6086
|
};
|
|
6029
6087
|
} finally {
|
|
6030
|
-
if (scratchDir)
|
|
6088
|
+
if (scratchDir) removeScratch(scratchDir);
|
|
6031
6089
|
}
|
|
6032
6090
|
}
|
|
6033
6091
|
function failResult(outputPath, target, source, error) {
|
|
@@ -6172,7 +6230,15 @@ function assertAudioNotInterrupted(failures) {
|
|
|
6172
6230
|
])
|
|
6173
6231
|
});
|
|
6174
6232
|
}
|
|
6175
|
-
|
|
6233
|
+
function runAudioStage(input) {
|
|
6234
|
+
return collectScratchLeftovers(
|
|
6235
|
+
() => runAudioStageScoped(input),
|
|
6236
|
+
(left) => {
|
|
6237
|
+
if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
|
|
6238
|
+
}
|
|
6239
|
+
);
|
|
6240
|
+
}
|
|
6241
|
+
async function runAudioStageScoped(input) {
|
|
6176
6242
|
const stageStart = Date.now();
|
|
6177
6243
|
const pcmSourcePath = join11(input.workDir, "audio-source.wav");
|
|
6178
6244
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
@@ -6194,7 +6260,7 @@ async function runAudioStage(input) {
|
|
|
6194
6260
|
}
|
|
6195
6261
|
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
6196
6262
|
} finally {
|
|
6197
|
-
|
|
6263
|
+
removeScratch2(pcmSourcePath);
|
|
6198
6264
|
}
|
|
6199
6265
|
}
|
|
6200
6266
|
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
@@ -6437,6 +6503,7 @@ async function runCaptureStage(input) {
|
|
|
6437
6503
|
dedupPerfs,
|
|
6438
6504
|
pageErrorLines: workerPageErrors,
|
|
6439
6505
|
openAttempt: input.openAttempt,
|
|
6506
|
+
verifyFirstSession: input.verifyFirstSession,
|
|
6440
6507
|
onProgress: (progress) => {
|
|
6441
6508
|
if (progress.latestWorkerPhase) {
|
|
6442
6509
|
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
@@ -6653,7 +6720,7 @@ import {
|
|
|
6653
6720
|
initializeSession as initializeSession7,
|
|
6654
6721
|
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
6655
6722
|
} from "@hyperframes/engine";
|
|
6656
|
-
import { fpsToNumber as
|
|
6723
|
+
import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
|
|
6657
6724
|
|
|
6658
6725
|
// src/services/hdrImageTransferCache.ts
|
|
6659
6726
|
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
@@ -6683,7 +6750,7 @@ import {
|
|
|
6683
6750
|
mkdirSync as mkdirSync5,
|
|
6684
6751
|
readdirSync as readdirSync2,
|
|
6685
6752
|
readFileSync as readFileSync5,
|
|
6686
|
-
rmSync as
|
|
6753
|
+
rmSync as rmSync4,
|
|
6687
6754
|
statSync as statSync4,
|
|
6688
6755
|
writeFileSync as writeFileSync3
|
|
6689
6756
|
} from "node:fs";
|
|
@@ -6822,7 +6889,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
6822
6889
|
fileSize
|
|
6823
6890
|
};
|
|
6824
6891
|
} finally {
|
|
6825
|
-
|
|
6892
|
+
rmSync4(input.palettePath, { force: true });
|
|
6826
6893
|
}
|
|
6827
6894
|
}
|
|
6828
6895
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -6846,6 +6913,8 @@ function videoEncoderOptions(input, engineCfg) {
|
|
|
6846
6913
|
preset: preset.preset,
|
|
6847
6914
|
quality: input.effectiveQuality,
|
|
6848
6915
|
bitrate: input.effectiveBitrate,
|
|
6916
|
+
maxBitrate: input.maxBitrate,
|
|
6917
|
+
bufferSize: input.bufferSize,
|
|
6849
6918
|
pixelFormat: preset.pixelFormat,
|
|
6850
6919
|
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
6851
6920
|
useGpu: job.config.useGpu,
|
|
@@ -7305,7 +7374,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
7305
7374
|
});
|
|
7306
7375
|
} : void 0,
|
|
7307
7376
|
void 0,
|
|
7308
|
-
options.cfg
|
|
7377
|
+
options.cfg,
|
|
7378
|
+
{ verifyFirstSession: options.verifyFirstSession }
|
|
7309
7379
|
);
|
|
7310
7380
|
pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
|
|
7311
7381
|
if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
|
|
@@ -8468,7 +8538,7 @@ import { join as join20 } from "node:path";
|
|
|
8468
8538
|
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
|
|
8469
8539
|
|
|
8470
8540
|
// src/services/distributed/mp4Stitch.ts
|
|
8471
|
-
import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, rmSync as
|
|
8541
|
+
import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, rmSync as rmSync6 } from "node:fs";
|
|
8472
8542
|
import { open } from "node:fs/promises";
|
|
8473
8543
|
import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
|
|
8474
8544
|
import { dirname as dirname10, join as join19 } from "node:path";
|
|
@@ -8846,7 +8916,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
|
8846
8916
|
renameWithRetry6(join19(workDir, "out.mp4"), outputPath);
|
|
8847
8917
|
} finally {
|
|
8848
8918
|
out.destroy();
|
|
8849
|
-
|
|
8919
|
+
rmSync6(workDir, { recursive: true, force: true });
|
|
8850
8920
|
}
|
|
8851
8921
|
}
|
|
8852
8922
|
|
|
@@ -9358,7 +9428,7 @@ var FREEZE_OWNED_PLAN_FILES = [
|
|
|
9358
9428
|
];
|
|
9359
9429
|
function removeFreezeOwnedPlanFiles(planDir) {
|
|
9360
9430
|
for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
|
|
9361
|
-
|
|
9431
|
+
rmSync7(join22(planDir, relativePath), { force: true });
|
|
9362
9432
|
}
|
|
9363
9433
|
}
|
|
9364
9434
|
function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
@@ -9418,6 +9488,9 @@ function buildLockedRenderConfig(input) {
|
|
|
9418
9488
|
preset,
|
|
9419
9489
|
crf: config.crf,
|
|
9420
9490
|
bitrate: config.bitrate,
|
|
9491
|
+
maxBitrate: config.maxBitrate,
|
|
9492
|
+
bufferSize: config.bufferSize,
|
|
9493
|
+
keyframeIntervalFrames: config.keyframeIntervalSeconds === void 0 ? void 0 : keyframeIntervalToGopSize3({ num: config.fps, den: 1 }, config.keyframeIntervalSeconds),
|
|
9421
9494
|
...locksVp9CpuUsed,
|
|
9422
9495
|
// GOP === chunkSize so every chunk's first frame is an IDR keyframe and
|
|
9423
9496
|
// ffmpeg concat-copy round-trips losslessly.
|
|
@@ -9569,7 +9642,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
9569
9642
|
throw new Error(`[plan] entry file not found: ${htmlPath}`);
|
|
9570
9643
|
}
|
|
9571
9644
|
const workDir = join22(planDir, ".plan-work");
|
|
9572
|
-
|
|
9645
|
+
rmSync7(workDir, { recursive: true, force: true });
|
|
9573
9646
|
mkdirSync10(workDir, { recursive: true });
|
|
9574
9647
|
const compiledDir = join22(workDir, "compiled");
|
|
9575
9648
|
mkdirSync10(compiledDir, { recursive: true });
|
|
@@ -9662,7 +9735,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
9662
9735
|
validateDistributedDuration({
|
|
9663
9736
|
duration: probeResult.duration,
|
|
9664
9737
|
totalFrames,
|
|
9665
|
-
fps:
|
|
9738
|
+
fps: fpsToNumber9(job.config.fps)
|
|
9666
9739
|
});
|
|
9667
9740
|
if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
|
|
9668
9741
|
if (probeResult.probeSession) {
|
|
@@ -9776,15 +9849,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
9776
9849
|
logPlanStage(log, "extract", stageStarted);
|
|
9777
9850
|
const stagedVideoFrames = join22(compiledDir, "__hyperframes_video_frames");
|
|
9778
9851
|
const videoFramesDst = join22(planDir, "video-frames");
|
|
9779
|
-
if (existsSync15(videoFramesDst))
|
|
9852
|
+
if (existsSync15(videoFramesDst)) rmSync7(videoFramesDst, { recursive: true, force: true });
|
|
9780
9853
|
const videoSourcesDir = join22(planDir, "video-sources");
|
|
9781
|
-
|
|
9854
|
+
rmSync7(videoSourcesDir, { recursive: true, force: true });
|
|
9782
9855
|
if (existsSync15(stagedVideoFrames)) {
|
|
9783
9856
|
renameWithRetry7(stagedVideoFrames, videoFramesDst);
|
|
9784
9857
|
} else {
|
|
9785
9858
|
mkdirSync10(videoFramesDst, { recursive: true });
|
|
9786
9859
|
}
|
|
9787
|
-
if (existsSync15(finalCompiledDir))
|
|
9860
|
+
if (existsSync15(finalCompiledDir)) rmSync7(finalCompiledDir, { recursive: true, force: true });
|
|
9788
9861
|
renameWithRetry7(compiledDir, finalCompiledDir);
|
|
9789
9862
|
const shipVideoSource = createVideoSourceShipper({
|
|
9790
9863
|
planDir,
|
|
@@ -9952,7 +10025,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
9952
10025
|
format: config.format
|
|
9953
10026
|
};
|
|
9954
10027
|
try {
|
|
9955
|
-
|
|
10028
|
+
rmSync7(workDir, { recursive: true, force: true });
|
|
9956
10029
|
} catch (err) {
|
|
9957
10030
|
log.warn("[plan] failed to remove temp work dir", {
|
|
9958
10031
|
workDir,
|
|
@@ -10022,7 +10095,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
|
|
|
10022
10095
|
renameWithRetry7(mixed.audioOutputPath, outputPath);
|
|
10023
10096
|
return { hasAudio: true };
|
|
10024
10097
|
} finally {
|
|
10025
|
-
|
|
10098
|
+
rmSync7(c.workDir, { recursive: true, force: true });
|
|
10026
10099
|
}
|
|
10027
10100
|
}
|
|
10028
10101
|
async function plan(projectDir, config, planDir, options = {}) {
|
|
@@ -10051,7 +10124,7 @@ import {
|
|
|
10051
10124
|
mkdtempSync as mkdtempSync4,
|
|
10052
10125
|
readFileSync as readFileSync10,
|
|
10053
10126
|
readdirSync as readdirSync7,
|
|
10054
|
-
rmSync as
|
|
10127
|
+
rmSync as rmSync9,
|
|
10055
10128
|
statSync as statSync7,
|
|
10056
10129
|
writeFileSync as writeFileSync9
|
|
10057
10130
|
} from "node:fs";
|
|
@@ -10067,7 +10140,7 @@ import {
|
|
|
10067
10140
|
linkSync as linkSync2,
|
|
10068
10141
|
mkdirSync as mkdirSync11,
|
|
10069
10142
|
mkdtempSync as mkdtempSync3,
|
|
10070
|
-
rmSync as
|
|
10143
|
+
rmSync as rmSync8,
|
|
10071
10144
|
statSync as statSync6,
|
|
10072
10145
|
writeFileSync as writeFileSync8
|
|
10073
10146
|
} from "node:fs";
|
|
@@ -10162,7 +10235,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
10162
10235
|
}
|
|
10163
10236
|
renameWithRetry8(temporaryPath, destinationPath);
|
|
10164
10237
|
} finally {
|
|
10165
|
-
|
|
10238
|
+
rmSync8(stagingDir, { recursive: true, force: true });
|
|
10166
10239
|
}
|
|
10167
10240
|
}
|
|
10168
10241
|
async commitManifest(manifestBytes) {
|
|
@@ -10179,7 +10252,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
10179
10252
|
}
|
|
10180
10253
|
async abort() {
|
|
10181
10254
|
if (!this.#committed) {
|
|
10182
|
-
|
|
10255
|
+
rmSync8(this.temporaryDir, { recursive: true, force: true });
|
|
10183
10256
|
}
|
|
10184
10257
|
}
|
|
10185
10258
|
};
|
|
@@ -10448,7 +10521,7 @@ function writeBlob(sourcePath, destinationPath) {
|
|
|
10448
10521
|
copyFileSync8(sourcePath, temporaryPath);
|
|
10449
10522
|
renameWithRetry9(temporaryPath, destinationPath);
|
|
10450
10523
|
} finally {
|
|
10451
|
-
|
|
10524
|
+
rmSync9(temporaryDir, { recursive: true, force: true });
|
|
10452
10525
|
}
|
|
10453
10526
|
}
|
|
10454
10527
|
function publishBlobFor(absolutePath, digests) {
|
|
@@ -10581,7 +10654,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
|
10581
10654
|
renameWithRetry9(tempDir, planV2Dir);
|
|
10582
10655
|
return resultFromManifest(planV2Dir, publication.manifest);
|
|
10583
10656
|
} catch (error) {
|
|
10584
|
-
|
|
10657
|
+
rmSync9(tempDir, { recursive: true, force: true });
|
|
10585
10658
|
throw error;
|
|
10586
10659
|
}
|
|
10587
10660
|
}
|
|
@@ -10648,7 +10721,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
10648
10721
|
}
|
|
10649
10722
|
throw error;
|
|
10650
10723
|
} finally {
|
|
10651
|
-
|
|
10724
|
+
rmSync9(stagingRoot, { recursive: true, force: true });
|
|
10652
10725
|
}
|
|
10653
10726
|
}
|
|
10654
10727
|
async function planV2(projectDir, config, planV2Dir) {
|
|
@@ -10865,7 +10938,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
10865
10938
|
);
|
|
10866
10939
|
renameWithRetry9(tempDir, destinationDir);
|
|
10867
10940
|
} catch (error) {
|
|
10868
|
-
|
|
10941
|
+
rmSync9(tempDir, { recursive: true, force: true });
|
|
10869
10942
|
throw error;
|
|
10870
10943
|
}
|
|
10871
10944
|
return {
|
|
@@ -10929,7 +11002,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
10929
11002
|
}
|
|
10930
11003
|
|
|
10931
11004
|
// src/services/distributed/planV2Execution.ts
|
|
10932
|
-
import { mkdtempSync as mkdtempSync6, rmSync as
|
|
11005
|
+
import { mkdtempSync as mkdtempSync6, rmSync as rmSync12 } from "node:fs";
|
|
10933
11006
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
10934
11007
|
import { join as join28 } from "node:path";
|
|
10935
11008
|
|
|
@@ -10941,12 +11014,13 @@ import {
|
|
|
10941
11014
|
mkdtempSync as mkdtempSync5,
|
|
10942
11015
|
readFileSync as readFileSync11,
|
|
10943
11016
|
readdirSync as readdirSync8,
|
|
10944
|
-
rmSync as
|
|
11017
|
+
rmSync as rmSync10,
|
|
10945
11018
|
statSync as statSync8,
|
|
10946
11019
|
writeFileSync as writeFileSync10
|
|
10947
11020
|
} from "node:fs";
|
|
10948
11021
|
import { dirname as dirname13, join as join26 } from "node:path";
|
|
10949
11022
|
import {
|
|
11023
|
+
appendRateCapArgs as appendRateCapArgs2,
|
|
10950
11024
|
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
10951
11025
|
describeFfmpegFailure as describeFfmpegFailure4,
|
|
10952
11026
|
getEncoderPreset as getEncoderPreset5,
|
|
@@ -11020,7 +11094,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
11020
11094
|
});
|
|
11021
11095
|
} finally {
|
|
11022
11096
|
try {
|
|
11023
|
-
|
|
11097
|
+
rmSync10(workDir, { recursive: true, force: true });
|
|
11024
11098
|
} catch (err) {
|
|
11025
11099
|
log.warn("[assemble] failed to remove work dir", {
|
|
11026
11100
|
workDir,
|
|
@@ -11082,6 +11156,9 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
|
|
|
11082
11156
|
"-r",
|
|
11083
11157
|
fpsArg
|
|
11084
11158
|
];
|
|
11159
|
+
if (encoder.keyframeIntervalFrames !== void 0)
|
|
11160
|
+
args.push("-g", String(encoder.keyframeIntervalFrames));
|
|
11161
|
+
appendRateCapArgs2(args, encoder);
|
|
11085
11162
|
appendSoftwareEncoderThreadsArg(args, "libx264");
|
|
11086
11163
|
return args;
|
|
11087
11164
|
}
|
|
@@ -11218,7 +11295,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
11218
11295
|
return encoderJson;
|
|
11219
11296
|
}
|
|
11220
11297
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
11221
|
-
if (existsSync18(outputPath))
|
|
11298
|
+
if (existsSync18(outputPath)) rmSync10(outputPath, { recursive: true, force: true });
|
|
11222
11299
|
mkdirSync13(outputPath, { recursive: true });
|
|
11223
11300
|
let globalIdx = 0;
|
|
11224
11301
|
for (const chunkDir of chunkPaths) {
|
|
@@ -11263,7 +11340,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
11263
11340
|
|
|
11264
11341
|
// src/services/distributed/renderChunk.ts
|
|
11265
11342
|
import { randomBytes } from "node:crypto";
|
|
11266
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as
|
|
11343
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
|
|
11267
11344
|
import { cpus } from "node:os";
|
|
11268
11345
|
import { join as join27 } from "node:path";
|
|
11269
11346
|
import {
|
|
@@ -11359,16 +11436,18 @@ function observedChunkCaptureMode(perfs) {
|
|
|
11359
11436
|
}
|
|
11360
11437
|
return mode;
|
|
11361
11438
|
}
|
|
11439
|
+
var PROBE_SELECTED_SCREENSHOT = "[renderChunk] probe selected screenshot capture";
|
|
11362
11440
|
function shouldRetryChunkCaptureWithScreenshot(error) {
|
|
11363
11441
|
const failure = classifyCaptureFailure4(error);
|
|
11364
11442
|
if (failure.kind === "cancelled" || failure.kind === "memory_exhaustion") return false;
|
|
11365
|
-
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);
|
|
11443
|
+
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);
|
|
11366
11444
|
}
|
|
11367
11445
|
async function runCaptureWithScreenshotFallback(input) {
|
|
11368
11446
|
try {
|
|
11369
11447
|
return await input.run(input.forceScreenshot);
|
|
11370
11448
|
} catch (error) {
|
|
11371
|
-
if (input.forceScreenshot ||
|
|
11449
|
+
if (input.forceScreenshot || await input.canRetry?.() === false) throw error;
|
|
11450
|
+
if (!shouldRetryChunkCaptureWithScreenshot(error)) throw error;
|
|
11372
11451
|
input.onFallback?.(error);
|
|
11373
11452
|
await input.resetForScreenshotRetry();
|
|
11374
11453
|
return await input.run(true);
|
|
@@ -11577,7 +11656,24 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11577
11656
|
let extraction;
|
|
11578
11657
|
const overlap = { current: null };
|
|
11579
11658
|
try {
|
|
11580
|
-
|
|
11659
|
+
const verifyInWorker = chunkWorkerCount > 1 && !forceScreenshotForChunk;
|
|
11660
|
+
let boxesMeasured;
|
|
11661
|
+
let boxesFailed;
|
|
11662
|
+
const boxesReady = new Promise((resolve10, reject) => {
|
|
11663
|
+
boxesMeasured = resolve10;
|
|
11664
|
+
boxesFailed = reject;
|
|
11665
|
+
});
|
|
11666
|
+
boxesReady.catch(() => {
|
|
11667
|
+
});
|
|
11668
|
+
const measureBoxes = async (page) => {
|
|
11669
|
+
shownBoxes = await measureVideoShownBoxes(
|
|
11670
|
+
page,
|
|
11671
|
+
[...shownFrames.keys()],
|
|
11672
|
+
chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
|
|
11673
|
+
);
|
|
11674
|
+
boxesMeasured();
|
|
11675
|
+
};
|
|
11676
|
+
if (chunkWorkerCount === 1) {
|
|
11581
11677
|
const bootStarted = Date.now();
|
|
11582
11678
|
session = await createVerifiedDistributedCaptureSession(
|
|
11583
11679
|
fileServer.url,
|
|
@@ -11586,11 +11682,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11586
11682
|
cfg
|
|
11587
11683
|
);
|
|
11588
11684
|
sessionBootMs = Date.now() - bootStarted;
|
|
11589
|
-
|
|
11590
|
-
session.page,
|
|
11591
|
-
[...shownFrames.keys()],
|
|
11592
|
-
chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
|
|
11593
|
-
);
|
|
11685
|
+
await measureBoxes(session.page);
|
|
11594
11686
|
const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
|
|
11595
11687
|
const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
|
|
11596
11688
|
if (browserSelectedScreenshot) {
|
|
@@ -11599,10 +11691,6 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11599
11691
|
"[renderChunk] Browser capability probe selected screenshot capture for the entire chunk",
|
|
11600
11692
|
{ chunkIndex }
|
|
11601
11693
|
);
|
|
11602
|
-
if (chunkWorkerCount > 1) {
|
|
11603
|
-
await closeCaptureSession10(session);
|
|
11604
|
-
session = null;
|
|
11605
|
-
}
|
|
11606
11694
|
} else if (beginFrameStalled) {
|
|
11607
11695
|
forceScreenshotForChunk = true;
|
|
11608
11696
|
log.warn(
|
|
@@ -11612,41 +11700,66 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11612
11700
|
await closeCaptureSession10(session).catch(() => {
|
|
11613
11701
|
});
|
|
11614
11702
|
session = null;
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11703
|
+
const screenshotBootStarted = Date.now();
|
|
11704
|
+
const screenshotCfg = {
|
|
11705
|
+
...cfg,
|
|
11706
|
+
forceScreenshot: true,
|
|
11707
|
+
forceScreenshotExplicitlyOptedOut: false
|
|
11708
|
+
};
|
|
11709
|
+
session = await createVerifiedDistributedCaptureSession(
|
|
11710
|
+
fileServer.url,
|
|
11711
|
+
framesDir,
|
|
11712
|
+
captureOptions,
|
|
11713
|
+
screenshotCfg
|
|
11714
|
+
);
|
|
11715
|
+
sessionBootMs += Date.now() - screenshotBootStarted;
|
|
11716
|
+
}
|
|
11717
|
+
} else if (!verifyInWorker) {
|
|
11718
|
+
boxesMeasured();
|
|
11719
|
+
}
|
|
11720
|
+
let verification;
|
|
11721
|
+
const verifyFirstWorker = (first) => verification ??= (async () => {
|
|
11722
|
+
sessionBootMs = Date.now() - verifyStarted;
|
|
11723
|
+
try {
|
|
11724
|
+
await measureBoxes(first.page);
|
|
11725
|
+
const selectedScreenshot = first.launchCaptureMode === "screenshot";
|
|
11726
|
+
if (selectedScreenshot || await beginFrameSessionNeedsScreenshotFallback(first)) {
|
|
11727
|
+
forceScreenshotForChunk = true;
|
|
11728
|
+
log.warn(
|
|
11729
|
+
selectedScreenshot ? "[renderChunk] Browser capability probe selected screenshot capture for the entire chunk" : "[renderChunk] BeginFrame liveness probe failed; using screenshot capture for the entire chunk",
|
|
11730
|
+
{ chunkIndex }
|
|
11627
11731
|
);
|
|
11628
|
-
|
|
11732
|
+
throw new Error(PROBE_SELECTED_SCREENSHOT);
|
|
11629
11733
|
}
|
|
11630
|
-
}
|
|
11631
|
-
|
|
11632
|
-
|
|
11734
|
+
} catch (error) {
|
|
11735
|
+
if (!forceScreenshotForChunk) {
|
|
11736
|
+
verifyFailure = error;
|
|
11737
|
+
boxesFailed(error);
|
|
11738
|
+
}
|
|
11739
|
+
throw error;
|
|
11633
11740
|
}
|
|
11634
|
-
}
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11741
|
+
})();
|
|
11742
|
+
let verifyStarted = 0;
|
|
11743
|
+
let verifyFailure;
|
|
11744
|
+
const deferredVideoFrames = planVideos ? boxesReady.then(() => {
|
|
11745
|
+
const extractStarted = Date.now();
|
|
11746
|
+
return extractDeferredVideoFramesForChunk({
|
|
11747
|
+
planDir,
|
|
11748
|
+
planVideos,
|
|
11749
|
+
frames: chunkFrameNumbers,
|
|
11750
|
+
fps: chunkFps,
|
|
11751
|
+
outputRoot: videoFramesDir,
|
|
11752
|
+
cfg,
|
|
11753
|
+
remoteSources,
|
|
11754
|
+
shownBoxes
|
|
11755
|
+
}).then((frames) => {
|
|
11756
|
+
videoExtractMs = Date.now() - extractStarted;
|
|
11757
|
+
return frames;
|
|
11758
|
+
});
|
|
11759
|
+
}) : boxesReady.then(() => []);
|
|
11649
11760
|
extraction = deferredVideoFrames;
|
|
11761
|
+
deferredVideoFrames.catch(() => {
|
|
11762
|
+
});
|
|
11650
11763
|
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
|
|
11651
11764
|
(deferred) => createFrameLookupTable5(
|
|
11652
11765
|
planVideos.videos,
|
|
@@ -11718,11 +11831,14 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11718
11831
|
// first frame is an IDR keyframe and concat-copy at assemble time
|
|
11719
11832
|
// round-trips losslessly.
|
|
11720
11833
|
lockGopForChunkConcat: !isPngSequence,
|
|
11721
|
-
gopSize: framesInChunk
|
|
11834
|
+
gopSize: chunkGopFrames(framesInChunk, encoder.keyframeIntervalFrames),
|
|
11835
|
+
maxBitrate: encoder.maxBitrate,
|
|
11836
|
+
bufferSize: encoder.bufferSize
|
|
11722
11837
|
};
|
|
11723
11838
|
const captureStarted = Date.now();
|
|
11724
11839
|
captureMode = await runCaptureWithScreenshotFallback({
|
|
11725
11840
|
forceScreenshot: forceScreenshotForChunk,
|
|
11841
|
+
canRetry: () => verifyFailure === void 0,
|
|
11726
11842
|
// fallow-ignore-next-line complexity
|
|
11727
11843
|
run: async (forceScreenshot) => {
|
|
11728
11844
|
const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : {
|
|
@@ -11752,6 +11868,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11752
11868
|
}
|
|
11753
11869
|
const captureSession = session;
|
|
11754
11870
|
session = null;
|
|
11871
|
+
verifyStarted = Date.now();
|
|
11755
11872
|
overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
|
|
11756
11873
|
await observeRenderStage(
|
|
11757
11874
|
new RenderObservabilityRecorder({ pipelineStartMs: start, log }),
|
|
@@ -11779,7 +11896,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11779
11896
|
frameRange: range,
|
|
11780
11897
|
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
|
|
11781
11898
|
interleaveFrames: interleavesCapture(overlap.current !== null, options),
|
|
11782
|
-
openAttempt
|
|
11899
|
+
openAttempt,
|
|
11900
|
+
verifyFirstSession: forceScreenshot || !verifyInWorker ? void 0 : verifyFirstWorker
|
|
11783
11901
|
}),
|
|
11784
11902
|
{ stall: frameStallRule(job) }
|
|
11785
11903
|
).catch(async (captureError) => {
|
|
@@ -11790,7 +11908,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11790
11908
|
return observedChunkCaptureMode(capturePerfs);
|
|
11791
11909
|
},
|
|
11792
11910
|
resetForScreenshotRetry: () => {
|
|
11793
|
-
|
|
11911
|
+
rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
11912
|
+
boxesMeasured();
|
|
11794
11913
|
mkdirSync14(framesDir, { recursive: true });
|
|
11795
11914
|
capturePerfs.length = 0;
|
|
11796
11915
|
captureAttempts.length = 0;
|
|
@@ -11806,6 +11925,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11806
11925
|
);
|
|
11807
11926
|
}
|
|
11808
11927
|
}).catch(async (captureError) => {
|
|
11928
|
+
if (verifyFailure !== void 0) throw verifyFailure;
|
|
11929
|
+
boxesFailed(captureError);
|
|
11809
11930
|
await deferredVideoFrames;
|
|
11810
11931
|
throw captureError;
|
|
11811
11932
|
});
|
|
@@ -11833,8 +11954,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11833
11954
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
11834
11955
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
11835
11956
|
});
|
|
11836
|
-
|
|
11837
|
-
|
|
11957
|
+
rmSync11(videoOnlyPath, { force: true });
|
|
11958
|
+
rmSync11(audioScratchPath, { force: true });
|
|
11838
11959
|
}
|
|
11839
11960
|
encodeStageMs = Date.now() - encodeStarted;
|
|
11840
11961
|
} finally {
|
|
@@ -11884,8 +12005,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11884
12005
|
writeFileSync11(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
11885
12006
|
`, "utf-8");
|
|
11886
12007
|
try {
|
|
11887
|
-
|
|
11888
|
-
|
|
12008
|
+
rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
12009
|
+
rmSync11(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
11889
12010
|
} catch (err) {
|
|
11890
12011
|
log.warn("[renderChunk] failed to remove work dir", {
|
|
11891
12012
|
workDir,
|
|
@@ -11920,7 +12041,7 @@ async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
|
|
|
11920
12041
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
11921
12042
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
|
|
11922
12043
|
} finally {
|
|
11923
|
-
|
|
12044
|
+
rmSync12(workRoot, { recursive: true, force: true });
|
|
11924
12045
|
}
|
|
11925
12046
|
}
|
|
11926
12047
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
@@ -11940,7 +12061,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
11940
12061
|
options
|
|
11941
12062
|
);
|
|
11942
12063
|
} finally {
|
|
11943
|
-
|
|
12064
|
+
rmSync12(workRoot, { recursive: true, force: true });
|
|
11944
12065
|
}
|
|
11945
12066
|
}
|
|
11946
12067
|
|
|
@@ -12033,6 +12154,20 @@ function validateDistributedRenderConfig(config) {
|
|
|
12033
12154
|
`must look like "10M" or "5000k"; got ${JSON.stringify(config.bitrate)}`
|
|
12034
12155
|
);
|
|
12035
12156
|
}
|
|
12157
|
+
try {
|
|
12158
|
+
validateDeliveryEncoderConfig({
|
|
12159
|
+
format: config.format,
|
|
12160
|
+
fps: { num: config.fps, den: 1 },
|
|
12161
|
+
keyframeIntervalSeconds: config.keyframeIntervalSeconds,
|
|
12162
|
+
maxBitrate: config.maxBitrate,
|
|
12163
|
+
bufferSize: config.bufferSize
|
|
12164
|
+
});
|
|
12165
|
+
} catch (error) {
|
|
12166
|
+
throw new InvalidConfigError(
|
|
12167
|
+
"config.maxBitrate",
|
|
12168
|
+
error instanceof Error ? error.message : String(error)
|
|
12169
|
+
);
|
|
12170
|
+
}
|
|
12036
12171
|
if (config.chunkSize !== void 0) {
|
|
12037
12172
|
if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
|
|
12038
12173
|
throw new InvalidConfigError(
|