@hyperframes/producer 0.9.11 → 0.9.13
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 +62 -57
- package/dist/index.js +281 -233
- package/dist/public-server.js +280 -232
- package/dist/services/render/captureCost.d.ts +3 -0
- package/dist/services/render/pageNeverReady.d.ts +6 -0
- package/package.json +7 -7
package/dist/public-server.js
CHANGED
|
@@ -1237,27 +1237,25 @@ function createMemorySampler(intervalMs = 250) {
|
|
|
1237
1237
|
};
|
|
1238
1238
|
}
|
|
1239
1239
|
var warnedSymlinkFallback = false;
|
|
1240
|
+
var copyCannotFix = (error) => ["ENOSPC", "EDQUOT", "EEXIST", "ENOENT"].includes(
|
|
1241
|
+
error?.code ?? ""
|
|
1242
|
+
);
|
|
1240
1243
|
function tryWindowsFrameJunction(fileSystem, src, dest) {
|
|
1241
1244
|
if (process.platform !== "win32") return false;
|
|
1242
1245
|
try {
|
|
1243
1246
|
fileSystem.symlinkSync(src, dest, "junction");
|
|
1244
1247
|
return true;
|
|
1245
1248
|
} catch (error) {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
code ?? ""
|
|
1249
|
-
)) {
|
|
1250
|
-
return false;
|
|
1251
|
-
}
|
|
1252
|
-
throw error;
|
|
1249
|
+
if (copyCannotFix(error)) throw error;
|
|
1250
|
+
return false;
|
|
1253
1251
|
}
|
|
1254
1252
|
}
|
|
1255
1253
|
function linkOrCopyFrameDir(fileSystem, src, dest) {
|
|
1256
1254
|
try {
|
|
1257
1255
|
fileSystem.symlinkSync(src, dest, "dir");
|
|
1258
1256
|
} catch (err) {
|
|
1257
|
+
if (copyCannotFix(err)) throw err;
|
|
1259
1258
|
const code = err?.code;
|
|
1260
|
-
if (code !== "EPERM" && code !== "EACCES" && code !== "UNKNOWN") throw err;
|
|
1261
1259
|
if (tryWindowsFrameJunction(fileSystem, src, dest)) return;
|
|
1262
1260
|
if (!warnedSymlinkFallback) {
|
|
1263
1261
|
warnedSymlinkFallback = true;
|
|
@@ -4870,203 +4868,8 @@ function startRenderEtaGuard(options) {
|
|
|
4870
4868
|
return stop;
|
|
4871
4869
|
}
|
|
4872
4870
|
|
|
4873
|
-
// src/services/render/segmentManifest.ts
|
|
4874
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
4875
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4876
|
-
import { join as join11 } from "node:path";
|
|
4877
|
-
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
4878
|
-
var MANIFEST_FILENAME = "segments.json";
|
|
4879
|
-
function computeSegmentPlanHash(input) {
|
|
4880
|
-
const ordered = [
|
|
4881
|
-
input.compositionHash,
|
|
4882
|
-
input.cliVersion,
|
|
4883
|
-
String(input.totalFrames),
|
|
4884
|
-
String(input.segmentFrames),
|
|
4885
|
-
`${input.fps.num}/${input.fps.den}`,
|
|
4886
|
-
`${input.width}x${input.height}`,
|
|
4887
|
-
input.codec,
|
|
4888
|
-
input.preset,
|
|
4889
|
-
input.quality === void 0 ? "" : String(input.quality),
|
|
4890
|
-
input.bitrate ?? "",
|
|
4891
|
-
input.pixelFormat ?? "",
|
|
4892
|
-
input.imageFormat,
|
|
4893
|
-
input.useGpu ? "gpu" : "cpu",
|
|
4894
|
-
`${input.outputWidth}x${input.outputHeight}`,
|
|
4895
|
-
input.motionBlur,
|
|
4896
|
-
input.pageMotionBlur ?? "",
|
|
4897
|
-
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
4898
|
-
];
|
|
4899
|
-
const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
|
|
4900
|
-
if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
|
|
4901
|
-
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
4902
|
-
}
|
|
4903
|
-
function segmentDirFor(projectRendersDir, planHash) {
|
|
4904
|
-
return join11(projectRendersDir, ".hf-segments", planHash);
|
|
4905
|
-
}
|
|
4906
|
-
function hasNumber(value, key) {
|
|
4907
|
-
return key in value && typeof Reflect.get(value, key) === "number";
|
|
4908
|
-
}
|
|
4909
|
-
function hasString(value, key) {
|
|
4910
|
-
return key in value && typeof Reflect.get(value, key) === "string";
|
|
4911
|
-
}
|
|
4912
|
-
function isEntry(value) {
|
|
4913
|
-
if (typeof value !== "object" || value === null) return false;
|
|
4914
|
-
return hasNumber(value, "index") && hasNumber(value, "startFrame") && hasNumber(value, "endFrame") && hasString(value, "path") && hasNumber(value, "bytes") && hasString(value, "completedAt");
|
|
4915
|
-
}
|
|
4916
|
-
function isManifest(value) {
|
|
4917
|
-
if (typeof value !== "object" || value === null) return false;
|
|
4918
|
-
const completed = Reflect.get(value, "completed");
|
|
4919
|
-
return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
|
|
4920
|
-
}
|
|
4921
|
-
function readSegmentManifest(segmentDir) {
|
|
4922
|
-
const path = join11(segmentDir, MANIFEST_FILENAME);
|
|
4923
|
-
if (!existsSync7(path)) return null;
|
|
4924
|
-
try {
|
|
4925
|
-
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
4926
|
-
return isManifest(parsed) ? parsed : null;
|
|
4927
|
-
} catch {
|
|
4928
|
-
return null;
|
|
4929
|
-
}
|
|
4930
|
-
}
|
|
4931
|
-
function writeSegmentManifest(segmentDir, manifest) {
|
|
4932
|
-
mkdirSync4(segmentDir, { recursive: true });
|
|
4933
|
-
const tmp = join11(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
|
|
4934
|
-
writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
|
|
4935
|
-
renameWithRetry2(tmp, join11(segmentDir, MANIFEST_FILENAME));
|
|
4936
|
-
}
|
|
4937
|
-
async function probeSegmentFrameCount(path, probe) {
|
|
4938
|
-
try {
|
|
4939
|
-
const meta = await probe(path);
|
|
4940
|
-
if (!Number.isFinite(meta.videoStreamDurationSeconds) || !Number.isFinite(meta.fps))
|
|
4941
|
-
return null;
|
|
4942
|
-
if (meta.fps <= 0) return null;
|
|
4943
|
-
return Math.round(meta.videoStreamDurationSeconds * meta.fps);
|
|
4944
|
-
} catch {
|
|
4945
|
-
return null;
|
|
4946
|
-
}
|
|
4947
|
-
}
|
|
4948
|
-
async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
|
|
4949
|
-
const ok = /* @__PURE__ */ new Set();
|
|
4950
|
-
if (manifest.planHash !== expectedHash) return ok;
|
|
4951
|
-
for (const entry of manifest.completed) {
|
|
4952
|
-
if (!existsSync7(entry.path)) continue;
|
|
4953
|
-
const size = statSync4(entry.path).size;
|
|
4954
|
-
if (size <= 0 || size !== entry.bytes) continue;
|
|
4955
|
-
const frames = await probeFrames(entry.path);
|
|
4956
|
-
if (frames !== entry.endFrame - entry.startFrame) continue;
|
|
4957
|
-
ok.add(entry.index);
|
|
4958
|
-
}
|
|
4959
|
-
return ok;
|
|
4960
|
-
}
|
|
4961
|
-
|
|
4962
|
-
// src/services/render/hdrMode.ts
|
|
4963
|
-
import {
|
|
4964
|
-
analyzeCompositionHdr,
|
|
4965
|
-
formatHdrAutoPromotionWarning,
|
|
4966
|
-
gradingKeepsSdr,
|
|
4967
|
-
HDR_AUTO_PROMOTION_PIPELINE,
|
|
4968
|
-
isHdrColorSpace,
|
|
4969
|
-
sanitizeHdrAutoPromotionAsset
|
|
4970
|
-
} from "@hyperframes/engine";
|
|
4971
|
-
function formatDowngradeWarning(outputFormat, forcedHdrWithoutSources) {
|
|
4972
|
-
const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
|
|
4973
|
-
const formatReason = outputFormat === "hls" ? "HLS output is SDR-only (H.264 in MPEG-TS)" : "HDR + alpha is not supported";
|
|
4974
|
-
return `[Render] ${hdrSourceReason}, but format is "${outputFormat}", so falling back to SDR. ${formatReason}. Use --format mp4 for HDR10 output.`;
|
|
4975
|
-
}
|
|
4976
|
-
function findRenderHdrAutoPromotionTrigger(input) {
|
|
4977
|
-
for (const extracted of input.extractionResult?.extracted ?? []) {
|
|
4978
|
-
if (!isHdrColorSpace(extracted.metadata.colorSpace)) continue;
|
|
4979
|
-
const source = input.videos.find(({ id }) => id === extracted.videoId)?.src;
|
|
4980
|
-
if (source) return source;
|
|
4981
|
-
}
|
|
4982
|
-
return input.images.find(({ id }) => input.nativeHdrImageIds.has(id))?.src;
|
|
4983
|
-
}
|
|
4984
|
-
function logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource) {
|
|
4985
|
-
const keptSdr = hasHdrSource && gradingKeepsSdr(hdrMode, input.media);
|
|
4986
|
-
if (keptSdr) {
|
|
4987
|
-
input.log.warn(
|
|
4988
|
-
"[Render] HDR source detected, but color grading is SDR-only, so rendering SDR: HDR sources are tone-mapped before the grade. Use --hdr to force HDR output (grades stay SDR)."
|
|
4989
|
-
);
|
|
4990
|
-
} else if (effectiveHdr && [...input.media.videos, ...input.media.images].some((m) => m.graded)) {
|
|
4991
|
-
input.log.warn(
|
|
4992
|
-
"[Render] HDR output with graded media: color grading is SDR-only, so graded elements are graded in SDR and placed in the HDR output."
|
|
4993
|
-
);
|
|
4994
|
-
}
|
|
4995
|
-
return keptSdr;
|
|
4996
|
-
}
|
|
4997
|
-
function logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, hasHdrSource) {
|
|
4998
|
-
const keptSdrForGrading = logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource);
|
|
4999
|
-
if (forcedHdrWithoutSources) {
|
|
5000
|
-
input.log.warn(
|
|
5001
|
-
"[Render] HDR forced by --hdr flag, but no HDR sources were detected, defaulting to HLG. SDR-only compositions may look perceptually wrong on HDR displays."
|
|
5002
|
-
);
|
|
5003
|
-
}
|
|
5004
|
-
if (!effectiveHdr) {
|
|
5005
|
-
if (keptSdrForGrading) return;
|
|
5006
|
-
input.log.info(
|
|
5007
|
-
hdrMode === "force-sdr" ? "[Render] SDR forced by --sdr flag" : "[Render] No HDR sources detected, rendering SDR"
|
|
5008
|
-
);
|
|
5009
|
-
return;
|
|
5010
|
-
}
|
|
5011
|
-
if (hdrMode === "auto") {
|
|
5012
|
-
input.log.warn(
|
|
5013
|
-
formatHdrAutoPromotionWarning({
|
|
5014
|
-
triggeringAsset: sanitizeHdrAutoPromotionAsset(
|
|
5015
|
-
input.autoPromotionTrigger ?? "an HDR-tagged asset"
|
|
5016
|
-
),
|
|
5017
|
-
output: HDR_AUTO_PROMOTION_PIPELINE
|
|
5018
|
-
})
|
|
5019
|
-
);
|
|
5020
|
-
return;
|
|
5021
|
-
}
|
|
5022
|
-
const reason = forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected, defaulting to HLG)" : "forced by --hdr flag";
|
|
5023
|
-
input.log.info(
|
|
5024
|
-
`[Render] HDR ${reason}, output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
5025
|
-
);
|
|
5026
|
-
}
|
|
5027
|
-
function formatCarriesHdr(format) {
|
|
5028
|
-
return format === "mp4";
|
|
5029
|
-
}
|
|
5030
|
-
function rendersSdrOnly(config, media) {
|
|
5031
|
-
return config.hdrMode === "force-sdr" || gradingKeepsSdr(config.hdrMode, media) || !formatCarriesHdr(config.format ?? "mp4");
|
|
5032
|
-
}
|
|
5033
|
-
function resolveEffectiveHdrMode(input) {
|
|
5034
|
-
const hdrMode = input.hdrMode ?? "auto";
|
|
5035
|
-
const videoColorSpaces = (input.extractionResult?.extracted ?? []).map(
|
|
5036
|
-
(ext) => ext.metadata.colorSpace
|
|
5037
|
-
);
|
|
5038
|
-
const allColorSpaces = [...videoColorSpaces, ...input.imageColorSpaces];
|
|
5039
|
-
const info = allColorSpaces.length > 0 ? analyzeCompositionHdr(allColorSpaces) : null;
|
|
5040
|
-
let effectiveHdr;
|
|
5041
|
-
let forcedHdrWithoutSources = false;
|
|
5042
|
-
if (hdrMode === "force-sdr" || gradingKeepsSdr(hdrMode, input.media)) {
|
|
5043
|
-
effectiveHdr = void 0;
|
|
5044
|
-
} else if (hdrMode === "force-hdr") {
|
|
5045
|
-
if (info?.hasHdr && info.dominantTransfer) {
|
|
5046
|
-
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5047
|
-
} else {
|
|
5048
|
-
effectiveHdr = { transfer: "hlg" };
|
|
5049
|
-
forcedHdrWithoutSources = true;
|
|
5050
|
-
}
|
|
5051
|
-
} else if (info?.hasHdr && info.dominantTransfer) {
|
|
5052
|
-
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5053
|
-
}
|
|
5054
|
-
if (effectiveHdr && !formatCarriesHdr(input.outputFormat)) {
|
|
5055
|
-
input.log.warn(formatDowngradeWarning(input.outputFormat, forcedHdrWithoutSources));
|
|
5056
|
-
effectiveHdr = void 0;
|
|
5057
|
-
}
|
|
5058
|
-
logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, info?.hasHdr === true);
|
|
5059
|
-
return effectiveHdr;
|
|
5060
|
-
}
|
|
5061
|
-
|
|
5062
|
-
// src/services/render/captureBeyondViewport.ts
|
|
5063
|
-
function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
5064
|
-
if (videoCount <= 0) return void 0;
|
|
5065
|
-
return true;
|
|
5066
|
-
}
|
|
5067
|
-
|
|
5068
4871
|
// src/services/render/captureCost.ts
|
|
5069
|
-
import { join as
|
|
4872
|
+
import { join as join11 } from "node:path";
|
|
5070
4873
|
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
5071
4874
|
import {
|
|
5072
4875
|
calculateOptimalWorkers,
|
|
@@ -5271,6 +5074,7 @@ async function runCaptureCalibration(input) {
|
|
|
5271
5074
|
let probeSession = input.probeSession;
|
|
5272
5075
|
let forceScreenshot = input.forceScreenshot;
|
|
5273
5076
|
let lastBrowserConsole = [];
|
|
5077
|
+
let calibrationEvaluateTimedOut = false;
|
|
5274
5078
|
const fps = fpsToNumber4(job.config.fps);
|
|
5275
5079
|
const sessionRef = { current: null };
|
|
5276
5080
|
const runOneCalibration = async (sessionDir, sessionCfg) => {
|
|
@@ -5311,11 +5115,12 @@ async function runCaptureCalibration(input) {
|
|
|
5311
5115
|
});
|
|
5312
5116
|
let calibration;
|
|
5313
5117
|
try {
|
|
5314
|
-
calibration = await runOneCalibration(
|
|
5118
|
+
calibration = await runOneCalibration(join11(workDir, "capture-calibration"), calibrationCfg);
|
|
5315
5119
|
} catch (error) {
|
|
5316
5120
|
const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
|
|
5317
5121
|
if (!shouldFallback) {
|
|
5318
5122
|
calibration = createFailedCaptureCalibrationEstimate("calibration-failed");
|
|
5123
|
+
calibrationEvaluateTimedOut = isEvaluateTimeout(error);
|
|
5319
5124
|
log.warn("[Render] Auto-worker calibration failed; using conservative worker budget.", {
|
|
5320
5125
|
protocolTimeout: calibrationCfg.protocolTimeout,
|
|
5321
5126
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -5344,11 +5149,12 @@ async function runCaptureCalibration(input) {
|
|
|
5344
5149
|
const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
|
|
5345
5150
|
try {
|
|
5346
5151
|
calibration = await runOneCalibration(
|
|
5347
|
-
|
|
5152
|
+
join11(workDir, "capture-calibration-screenshot"),
|
|
5348
5153
|
screenshotCfg
|
|
5349
5154
|
);
|
|
5350
5155
|
} catch (fallbackError) {
|
|
5351
5156
|
calibration = createFailedCaptureCalibrationEstimate("calibration-screenshot-failed");
|
|
5157
|
+
calibrationEvaluateTimedOut = isEvaluateTimeout(fallbackError);
|
|
5352
5158
|
log.warn(
|
|
5353
5159
|
"[Render] Screenshot auto-worker calibration failed after BeginFrame fallback; using conservative worker budget.",
|
|
5354
5160
|
{
|
|
@@ -5365,7 +5171,16 @@ async function runCaptureCalibration(input) {
|
|
|
5365
5171
|
});
|
|
5366
5172
|
}
|
|
5367
5173
|
}
|
|
5368
|
-
return {
|
|
5174
|
+
return {
|
|
5175
|
+
calibration,
|
|
5176
|
+
forceScreenshot,
|
|
5177
|
+
probeSession,
|
|
5178
|
+
lastBrowserConsole,
|
|
5179
|
+
calibrationEvaluateTimedOut
|
|
5180
|
+
};
|
|
5181
|
+
}
|
|
5182
|
+
function isEvaluateTimeout(error) {
|
|
5183
|
+
return /Runtime\.evaluate timed out/i.test(normalizeErrorMessage(error));
|
|
5369
5184
|
}
|
|
5370
5185
|
function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
5371
5186
|
const message = normalizeErrorMessage(error);
|
|
@@ -5374,6 +5189,224 @@ function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
|
5374
5189
|
);
|
|
5375
5190
|
}
|
|
5376
5191
|
|
|
5192
|
+
// src/services/render/pageNeverReady.ts
|
|
5193
|
+
var PAGE_NEVER_READY = "PAGE_NEVER_READY";
|
|
5194
|
+
var PageNeverReadyError = class extends Error {
|
|
5195
|
+
code = PAGE_NEVER_READY;
|
|
5196
|
+
// The producer server reads these off the error for the wire envelope.
|
|
5197
|
+
owner = "user";
|
|
5198
|
+
retryable = false;
|
|
5199
|
+
constructor(cause) {
|
|
5200
|
+
super(
|
|
5201
|
+
"The composition page never became ready: it did not answer browser commands during calibration or capture, and no frame was captured. Its script may never return.",
|
|
5202
|
+
{ cause }
|
|
5203
|
+
);
|
|
5204
|
+
this.name = "PageNeverReadyError";
|
|
5205
|
+
}
|
|
5206
|
+
};
|
|
5207
|
+
function checkPageNeverReady(input) {
|
|
5208
|
+
const { calibrationEvaluateTimedOut, framesRendered, error } = input;
|
|
5209
|
+
if (!calibrationEvaluateTimedOut || framesRendered > 0 || !isEvaluateTimeout(error)) {
|
|
5210
|
+
return void 0;
|
|
5211
|
+
}
|
|
5212
|
+
return new PageNeverReadyError(error);
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
// src/services/render/segmentManifest.ts
|
|
5216
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5217
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5218
|
+
import { join as join12 } from "node:path";
|
|
5219
|
+
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
5220
|
+
var MANIFEST_FILENAME = "segments.json";
|
|
5221
|
+
function computeSegmentPlanHash(input) {
|
|
5222
|
+
const ordered = [
|
|
5223
|
+
input.compositionHash,
|
|
5224
|
+
input.cliVersion,
|
|
5225
|
+
String(input.totalFrames),
|
|
5226
|
+
String(input.segmentFrames),
|
|
5227
|
+
`${input.fps.num}/${input.fps.den}`,
|
|
5228
|
+
`${input.width}x${input.height}`,
|
|
5229
|
+
input.codec,
|
|
5230
|
+
input.preset,
|
|
5231
|
+
input.quality === void 0 ? "" : String(input.quality),
|
|
5232
|
+
input.bitrate ?? "",
|
|
5233
|
+
input.pixelFormat ?? "",
|
|
5234
|
+
input.imageFormat,
|
|
5235
|
+
input.useGpu ? "gpu" : "cpu",
|
|
5236
|
+
`${input.outputWidth}x${input.outputHeight}`,
|
|
5237
|
+
input.motionBlur,
|
|
5238
|
+
input.pageMotionBlur ?? "",
|
|
5239
|
+
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
5240
|
+
];
|
|
5241
|
+
const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
|
|
5242
|
+
if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
|
|
5243
|
+
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
5244
|
+
}
|
|
5245
|
+
function segmentDirFor(projectRendersDir, planHash) {
|
|
5246
|
+
return join12(projectRendersDir, ".hf-segments", planHash);
|
|
5247
|
+
}
|
|
5248
|
+
function hasNumber(value, key) {
|
|
5249
|
+
return key in value && typeof Reflect.get(value, key) === "number";
|
|
5250
|
+
}
|
|
5251
|
+
function hasString(value, key) {
|
|
5252
|
+
return key in value && typeof Reflect.get(value, key) === "string";
|
|
5253
|
+
}
|
|
5254
|
+
function isEntry(value) {
|
|
5255
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5256
|
+
return hasNumber(value, "index") && hasNumber(value, "startFrame") && hasNumber(value, "endFrame") && hasString(value, "path") && hasNumber(value, "bytes") && hasString(value, "completedAt");
|
|
5257
|
+
}
|
|
5258
|
+
function isManifest(value) {
|
|
5259
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5260
|
+
const completed = Reflect.get(value, "completed");
|
|
5261
|
+
return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
|
|
5262
|
+
}
|
|
5263
|
+
function readSegmentManifest(segmentDir) {
|
|
5264
|
+
const path = join12(segmentDir, MANIFEST_FILENAME);
|
|
5265
|
+
if (!existsSync7(path)) return null;
|
|
5266
|
+
try {
|
|
5267
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
5268
|
+
return isManifest(parsed) ? parsed : null;
|
|
5269
|
+
} catch {
|
|
5270
|
+
return null;
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
function writeSegmentManifest(segmentDir, manifest) {
|
|
5274
|
+
mkdirSync4(segmentDir, { recursive: true });
|
|
5275
|
+
const tmp = join12(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
|
|
5276
|
+
writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
|
|
5277
|
+
renameWithRetry2(tmp, join12(segmentDir, MANIFEST_FILENAME));
|
|
5278
|
+
}
|
|
5279
|
+
async function probeSegmentFrameCount(path, probe) {
|
|
5280
|
+
try {
|
|
5281
|
+
const meta = await probe(path);
|
|
5282
|
+
if (!Number.isFinite(meta.videoStreamDurationSeconds) || !Number.isFinite(meta.fps))
|
|
5283
|
+
return null;
|
|
5284
|
+
if (meta.fps <= 0) return null;
|
|
5285
|
+
return Math.round(meta.videoStreamDurationSeconds * meta.fps);
|
|
5286
|
+
} catch {
|
|
5287
|
+
return null;
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
|
|
5291
|
+
const ok = /* @__PURE__ */ new Set();
|
|
5292
|
+
if (manifest.planHash !== expectedHash) return ok;
|
|
5293
|
+
for (const entry of manifest.completed) {
|
|
5294
|
+
if (!existsSync7(entry.path)) continue;
|
|
5295
|
+
const size = statSync4(entry.path).size;
|
|
5296
|
+
if (size <= 0 || size !== entry.bytes) continue;
|
|
5297
|
+
const frames = await probeFrames(entry.path);
|
|
5298
|
+
if (frames !== entry.endFrame - entry.startFrame) continue;
|
|
5299
|
+
ok.add(entry.index);
|
|
5300
|
+
}
|
|
5301
|
+
return ok;
|
|
5302
|
+
}
|
|
5303
|
+
|
|
5304
|
+
// src/services/render/hdrMode.ts
|
|
5305
|
+
import {
|
|
5306
|
+
analyzeCompositionHdr,
|
|
5307
|
+
formatHdrAutoPromotionWarning,
|
|
5308
|
+
gradingKeepsSdr,
|
|
5309
|
+
HDR_AUTO_PROMOTION_PIPELINE,
|
|
5310
|
+
isHdrColorSpace,
|
|
5311
|
+
sanitizeHdrAutoPromotionAsset
|
|
5312
|
+
} from "@hyperframes/engine";
|
|
5313
|
+
function formatDowngradeWarning(outputFormat, forcedHdrWithoutSources) {
|
|
5314
|
+
const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
|
|
5315
|
+
const formatReason = outputFormat === "hls" ? "HLS output is SDR-only (H.264 in MPEG-TS)" : "HDR + alpha is not supported";
|
|
5316
|
+
return `[Render] ${hdrSourceReason}, but format is "${outputFormat}", so falling back to SDR. ${formatReason}. Use --format mp4 for HDR10 output.`;
|
|
5317
|
+
}
|
|
5318
|
+
function findRenderHdrAutoPromotionTrigger(input) {
|
|
5319
|
+
for (const extracted of input.extractionResult?.extracted ?? []) {
|
|
5320
|
+
if (!isHdrColorSpace(extracted.metadata.colorSpace)) continue;
|
|
5321
|
+
const source = input.videos.find(({ id }) => id === extracted.videoId)?.src;
|
|
5322
|
+
if (source) return source;
|
|
5323
|
+
}
|
|
5324
|
+
return input.images.find(({ id }) => input.nativeHdrImageIds.has(id))?.src;
|
|
5325
|
+
}
|
|
5326
|
+
function logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource) {
|
|
5327
|
+
const keptSdr = hasHdrSource && gradingKeepsSdr(hdrMode, input.media);
|
|
5328
|
+
if (keptSdr) {
|
|
5329
|
+
input.log.warn(
|
|
5330
|
+
"[Render] HDR source detected, but color grading is SDR-only, so rendering SDR: HDR sources are tone-mapped before the grade. Use --hdr to force HDR output (grades stay SDR)."
|
|
5331
|
+
);
|
|
5332
|
+
} else if (effectiveHdr && [...input.media.videos, ...input.media.images].some((m) => m.graded)) {
|
|
5333
|
+
input.log.warn(
|
|
5334
|
+
"[Render] HDR output with graded media: color grading is SDR-only, so graded elements are graded in SDR and placed in the HDR output."
|
|
5335
|
+
);
|
|
5336
|
+
}
|
|
5337
|
+
return keptSdr;
|
|
5338
|
+
}
|
|
5339
|
+
function logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, hasHdrSource) {
|
|
5340
|
+
const keptSdrForGrading = logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource);
|
|
5341
|
+
if (forcedHdrWithoutSources) {
|
|
5342
|
+
input.log.warn(
|
|
5343
|
+
"[Render] HDR forced by --hdr flag, but no HDR sources were detected, defaulting to HLG. SDR-only compositions may look perceptually wrong on HDR displays."
|
|
5344
|
+
);
|
|
5345
|
+
}
|
|
5346
|
+
if (!effectiveHdr) {
|
|
5347
|
+
if (keptSdrForGrading) return;
|
|
5348
|
+
input.log.info(
|
|
5349
|
+
hdrMode === "force-sdr" ? "[Render] SDR forced by --sdr flag" : "[Render] No HDR sources detected, rendering SDR"
|
|
5350
|
+
);
|
|
5351
|
+
return;
|
|
5352
|
+
}
|
|
5353
|
+
if (hdrMode === "auto") {
|
|
5354
|
+
input.log.warn(
|
|
5355
|
+
formatHdrAutoPromotionWarning({
|
|
5356
|
+
triggeringAsset: sanitizeHdrAutoPromotionAsset(
|
|
5357
|
+
input.autoPromotionTrigger ?? "an HDR-tagged asset"
|
|
5358
|
+
),
|
|
5359
|
+
output: HDR_AUTO_PROMOTION_PIPELINE
|
|
5360
|
+
})
|
|
5361
|
+
);
|
|
5362
|
+
return;
|
|
5363
|
+
}
|
|
5364
|
+
const reason = forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected, defaulting to HLG)" : "forced by --hdr flag";
|
|
5365
|
+
input.log.info(
|
|
5366
|
+
`[Render] HDR ${reason}, output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
5367
|
+
);
|
|
5368
|
+
}
|
|
5369
|
+
function formatCarriesHdr(format) {
|
|
5370
|
+
return format === "mp4";
|
|
5371
|
+
}
|
|
5372
|
+
function rendersSdrOnly(config, media) {
|
|
5373
|
+
return config.hdrMode === "force-sdr" || gradingKeepsSdr(config.hdrMode, media) || !formatCarriesHdr(config.format ?? "mp4");
|
|
5374
|
+
}
|
|
5375
|
+
function resolveEffectiveHdrMode(input) {
|
|
5376
|
+
const hdrMode = input.hdrMode ?? "auto";
|
|
5377
|
+
const videoColorSpaces = (input.extractionResult?.extracted ?? []).map(
|
|
5378
|
+
(ext) => ext.metadata.colorSpace
|
|
5379
|
+
);
|
|
5380
|
+
const allColorSpaces = [...videoColorSpaces, ...input.imageColorSpaces];
|
|
5381
|
+
const info = allColorSpaces.length > 0 ? analyzeCompositionHdr(allColorSpaces) : null;
|
|
5382
|
+
let effectiveHdr;
|
|
5383
|
+
let forcedHdrWithoutSources = false;
|
|
5384
|
+
if (hdrMode === "force-sdr" || gradingKeepsSdr(hdrMode, input.media)) {
|
|
5385
|
+
effectiveHdr = void 0;
|
|
5386
|
+
} else if (hdrMode === "force-hdr") {
|
|
5387
|
+
if (info?.hasHdr && info.dominantTransfer) {
|
|
5388
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5389
|
+
} else {
|
|
5390
|
+
effectiveHdr = { transfer: "hlg" };
|
|
5391
|
+
forcedHdrWithoutSources = true;
|
|
5392
|
+
}
|
|
5393
|
+
} else if (info?.hasHdr && info.dominantTransfer) {
|
|
5394
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5395
|
+
}
|
|
5396
|
+
if (effectiveHdr && !formatCarriesHdr(input.outputFormat)) {
|
|
5397
|
+
input.log.warn(formatDowngradeWarning(input.outputFormat, forcedHdrWithoutSources));
|
|
5398
|
+
effectiveHdr = void 0;
|
|
5399
|
+
}
|
|
5400
|
+
logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, info?.hasHdr === true);
|
|
5401
|
+
return effectiveHdr;
|
|
5402
|
+
}
|
|
5403
|
+
|
|
5404
|
+
// src/services/render/captureBeyondViewport.ts
|
|
5405
|
+
function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
5406
|
+
if (videoCount <= 0) return void 0;
|
|
5407
|
+
return true;
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5377
5410
|
// src/services/render/observability.ts
|
|
5378
5411
|
import { createHash as createHash3 } from "node:crypto";
|
|
5379
5412
|
import { redactTelemetryString } from "@hyperframes/core";
|
|
@@ -9473,13 +9506,20 @@ var VideoExtractionStageError = class extends Error {
|
|
|
9473
9506
|
*/
|
|
9474
9507
|
publicMetadata;
|
|
9475
9508
|
};
|
|
9476
|
-
function
|
|
9509
|
+
function throwIfHostFailure(result) {
|
|
9477
9510
|
const interrupted = result.errors.find((failure) => failure.kind === "external_interruption");
|
|
9478
|
-
if (
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9511
|
+
if (interrupted) {
|
|
9512
|
+
throw encoderFailureError("Video frame extraction failed", {
|
|
9513
|
+
error: String(interrupted.error),
|
|
9514
|
+
failureReason: "external_interruption"
|
|
9515
|
+
});
|
|
9516
|
+
}
|
|
9517
|
+
const full = result.errors.filter((failure) => failure.kind === "disk_full").length;
|
|
9518
|
+
if (full === 0) return;
|
|
9519
|
+
throw Object.assign(
|
|
9520
|
+
new Error(`ENOSPC: no space left on device, extracting the frames of ${full} video source(s)`),
|
|
9521
|
+
{ code: "ENOSPC" }
|
|
9522
|
+
);
|
|
9483
9523
|
}
|
|
9484
9524
|
function buildVideoExtractionStageError(result) {
|
|
9485
9525
|
if (result.success && result.errors.length === 0) return null;
|
|
@@ -9694,7 +9734,7 @@ async function runExtractVideosStage(input) {
|
|
|
9694
9734
|
);
|
|
9695
9735
|
extractionResult.phaseBreakdown.transientRetries = (extractionResult.phaseBreakdown.transientRetries ?? 0) + hdrProbeTransientRetries;
|
|
9696
9736
|
assertNotAborted();
|
|
9697
|
-
|
|
9737
|
+
throwIfHostFailure(extractionResult);
|
|
9698
9738
|
failureToEnforce = applyVideoExtractionFailurePolicy(extractionResult, extractionPolicy, log);
|
|
9699
9739
|
materializeExtractedFramesForCompiledDir(extractionResult.extracted, compiledDir, {
|
|
9700
9740
|
materializeSymlinks
|
|
@@ -12832,6 +12872,22 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
12832
12872
|
}
|
|
12833
12873
|
const remainingCount = countFrameRanges(remaining);
|
|
12834
12874
|
const madeProgress = captureAttemptMadeProgress(frameCount, remainingCount);
|
|
12875
|
+
if (options.allowRetry && !madeProgress && (currentWorkers > 1 || initializationRetriesUsed === 0 && failure.kind === "protocol_timeout") && /\bNetwork\.enable timed out|Navigation timeout of \d+ ms exceeded/i.test(failure.message)) {
|
|
12876
|
+
initializationRetriesUsed++;
|
|
12877
|
+
const nextWorkers2 = getNextRetryWorkerCount(currentWorkers);
|
|
12878
|
+
options.log.warn(
|
|
12879
|
+
"[Render] Browser initialization timed out; retrying with fewer workers.",
|
|
12880
|
+
{
|
|
12881
|
+
fromWorkers: currentWorkers,
|
|
12882
|
+
toWorkers: nextWorkers2,
|
|
12883
|
+
error: failure.message
|
|
12884
|
+
}
|
|
12885
|
+
);
|
|
12886
|
+
currentWorkers = nextWorkers2;
|
|
12887
|
+
missingRanges = remaining;
|
|
12888
|
+
attempt++;
|
|
12889
|
+
continue;
|
|
12890
|
+
}
|
|
12835
12891
|
if (options.allowRetry && isTransientCaptureRetryEligible(failure.kind, remaining, transientRetriesUsed)) {
|
|
12836
12892
|
transientRetriesUsed++;
|
|
12837
12893
|
options.log.warn(
|
|
@@ -12849,22 +12905,6 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
12849
12905
|
pendingTransientRetry = true;
|
|
12850
12906
|
continue;
|
|
12851
12907
|
}
|
|
12852
|
-
if (options.allowRetry && !madeProgress && initializationRetriesUsed === 0 && failure.kind === "protocol_timeout" && /\bNetwork\.enable timed out/i.test(failure.message)) {
|
|
12853
|
-
initializationRetriesUsed++;
|
|
12854
|
-
const nextWorkers2 = getNextRetryWorkerCount(currentWorkers);
|
|
12855
|
-
options.log.warn(
|
|
12856
|
-
"[Render] Browser initialization timed out; retrying once with fresh sessions.",
|
|
12857
|
-
{
|
|
12858
|
-
fromWorkers: currentWorkers,
|
|
12859
|
-
toWorkers: nextWorkers2,
|
|
12860
|
-
error: failure.message
|
|
12861
|
-
}
|
|
12862
|
-
);
|
|
12863
|
-
currentWorkers = nextWorkers2;
|
|
12864
|
-
missingRanges = remaining;
|
|
12865
|
-
attempt++;
|
|
12866
|
-
continue;
|
|
12867
|
-
}
|
|
12868
12908
|
if (!madeProgress) {
|
|
12869
12909
|
options.log.warn(
|
|
12870
12910
|
"[Render] Capture attempt made no forward progress; composition is likely structurally broken, not retrying.",
|
|
@@ -13422,6 +13462,7 @@ async function executeRenderPipeline(input) {
|
|
|
13422
13462
|
const onProgress = execution.onProgress;
|
|
13423
13463
|
const tooSlow = new AbortController();
|
|
13424
13464
|
let tooSlowError;
|
|
13465
|
+
let calibrationEvaluateTimedOut = false;
|
|
13425
13466
|
const executionSignal = job.config.maxRenderMs ? AbortSignal.any([tooSlow.signal, ...execution.signal ? [execution.signal] : []]) : execution.signal;
|
|
13426
13467
|
let fileServer = null;
|
|
13427
13468
|
let probeSession = null;
|
|
@@ -14164,6 +14205,7 @@ async function executeRenderPipeline(input) {
|
|
|
14164
14205
|
{ heartbeatMessage: "browser calibrating (frames not started)" }
|
|
14165
14206
|
);
|
|
14166
14207
|
captureCalibration = outcome.calibration;
|
|
14208
|
+
calibrationEvaluateTimedOut = outcome.calibrationEvaluateTimedOut;
|
|
14167
14209
|
captureForceScreenshot = outcome.forceScreenshot;
|
|
14168
14210
|
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
|
14169
14211
|
probeSession = outcome.probeSession;
|
|
@@ -15314,7 +15356,12 @@ async function executeRenderPipeline(input) {
|
|
|
15314
15356
|
await eventPublisher.flush();
|
|
15315
15357
|
} catch (caught) {
|
|
15316
15358
|
await pendingAudio;
|
|
15317
|
-
const
|
|
15359
|
+
const pageNeverReady = checkPageNeverReady({
|
|
15360
|
+
calibrationEvaluateTimedOut,
|
|
15361
|
+
framesRendered: job.framesRendered ?? 0,
|
|
15362
|
+
error: caught
|
|
15363
|
+
});
|
|
15364
|
+
const error = surfaceScriptFailure(tooSlowError ?? pageNeverReady ?? caught, job);
|
|
15318
15365
|
if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
|
|
15319
15366
|
job.error = error instanceof Error ? error.message : "render_cancelled";
|
|
15320
15367
|
updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
|
|
@@ -16013,7 +16060,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
16013
16060
|
"VIDEO_EXTRACTION_FAILED",
|
|
16014
16061
|
"ENCODER_INTERRUPTED",
|
|
16015
16062
|
"RENDER_TOO_SLOW",
|
|
16016
|
-
"RENDER_STAGE_STALLED"
|
|
16063
|
+
"RENDER_STAGE_STALLED",
|
|
16064
|
+
"PAGE_NEVER_READY"
|
|
16017
16065
|
]);
|
|
16018
16066
|
function extractSafeRenderErrorCode(error) {
|
|
16019
16067
|
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
|
@@ -86,6 +86,8 @@ export interface CaptureCalibrationOutcome {
|
|
|
86
86
|
probeSession: CaptureSession | null;
|
|
87
87
|
/** Buffer of whichever session was active last; the sequencer uses it for the error-path tail. */
|
|
88
88
|
lastBrowserConsole: string[];
|
|
89
|
+
/** The final calibration failure was a `Runtime.evaluate` timeout: the page never answered. */
|
|
90
|
+
calibrationEvaluateTimedOut: boolean;
|
|
89
91
|
}
|
|
90
92
|
/**
|
|
91
93
|
* Run the auto-worker capture-cost calibration, including the
|
|
@@ -108,6 +110,7 @@ export declare function runCaptureCalibration(input: {
|
|
|
108
110
|
/** Throws `RenderCancelledError` when the caller's abort signal fires. */
|
|
109
111
|
assertNotAborted: () => void;
|
|
110
112
|
}): Promise<CaptureCalibrationOutcome>;
|
|
113
|
+
export declare function isEvaluateTimeout(error: unknown): boolean;
|
|
111
114
|
/**
|
|
112
115
|
* Same as `runCaptureCalibration`'s error-classification check, but
|
|
113
116
|
* exported separately because the sequencer also calls it from the
|