@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/index.js
CHANGED
|
@@ -1223,27 +1223,25 @@ function createMemorySampler(intervalMs = 250) {
|
|
|
1223
1223
|
};
|
|
1224
1224
|
}
|
|
1225
1225
|
var warnedSymlinkFallback = false;
|
|
1226
|
+
var copyCannotFix = (error) => ["ENOSPC", "EDQUOT", "EEXIST", "ENOENT"].includes(
|
|
1227
|
+
error?.code ?? ""
|
|
1228
|
+
);
|
|
1226
1229
|
function tryWindowsFrameJunction(fileSystem, src, dest) {
|
|
1227
1230
|
if (process.platform !== "win32") return false;
|
|
1228
1231
|
try {
|
|
1229
1232
|
fileSystem.symlinkSync(src, dest, "junction");
|
|
1230
1233
|
return true;
|
|
1231
1234
|
} catch (error) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
code ?? ""
|
|
1235
|
-
)) {
|
|
1236
|
-
return false;
|
|
1237
|
-
}
|
|
1238
|
-
throw error;
|
|
1235
|
+
if (copyCannotFix(error)) throw error;
|
|
1236
|
+
return false;
|
|
1239
1237
|
}
|
|
1240
1238
|
}
|
|
1241
1239
|
function linkOrCopyFrameDir(fileSystem, src, dest) {
|
|
1242
1240
|
try {
|
|
1243
1241
|
fileSystem.symlinkSync(src, dest, "dir");
|
|
1244
1242
|
} catch (err) {
|
|
1243
|
+
if (copyCannotFix(err)) throw err;
|
|
1245
1244
|
const code = err?.code;
|
|
1246
|
-
if (code !== "EPERM" && code !== "EACCES" && code !== "UNKNOWN") throw err;
|
|
1247
1245
|
if (tryWindowsFrameJunction(fileSystem, src, dest)) return;
|
|
1248
1246
|
if (!warnedSymlinkFallback) {
|
|
1249
1247
|
warnedSymlinkFallback = true;
|
|
@@ -4857,203 +4855,8 @@ function startRenderEtaGuard(options) {
|
|
|
4857
4855
|
return stop;
|
|
4858
4856
|
}
|
|
4859
4857
|
|
|
4860
|
-
// src/services/render/segmentManifest.ts
|
|
4861
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
4862
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4863
|
-
import { join as join11 } from "node:path";
|
|
4864
|
-
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
4865
|
-
var MANIFEST_FILENAME = "segments.json";
|
|
4866
|
-
function computeSegmentPlanHash(input) {
|
|
4867
|
-
const ordered = [
|
|
4868
|
-
input.compositionHash,
|
|
4869
|
-
input.cliVersion,
|
|
4870
|
-
String(input.totalFrames),
|
|
4871
|
-
String(input.segmentFrames),
|
|
4872
|
-
`${input.fps.num}/${input.fps.den}`,
|
|
4873
|
-
`${input.width}x${input.height}`,
|
|
4874
|
-
input.codec,
|
|
4875
|
-
input.preset,
|
|
4876
|
-
input.quality === void 0 ? "" : String(input.quality),
|
|
4877
|
-
input.bitrate ?? "",
|
|
4878
|
-
input.pixelFormat ?? "",
|
|
4879
|
-
input.imageFormat,
|
|
4880
|
-
input.useGpu ? "gpu" : "cpu",
|
|
4881
|
-
`${input.outputWidth}x${input.outputHeight}`,
|
|
4882
|
-
input.motionBlur,
|
|
4883
|
-
input.pageMotionBlur ?? "",
|
|
4884
|
-
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
4885
|
-
];
|
|
4886
|
-
const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
|
|
4887
|
-
if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
|
|
4888
|
-
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
4889
|
-
}
|
|
4890
|
-
function segmentDirFor(projectRendersDir, planHash) {
|
|
4891
|
-
return join11(projectRendersDir, ".hf-segments", planHash);
|
|
4892
|
-
}
|
|
4893
|
-
function hasNumber(value, key) {
|
|
4894
|
-
return key in value && typeof Reflect.get(value, key) === "number";
|
|
4895
|
-
}
|
|
4896
|
-
function hasString(value, key) {
|
|
4897
|
-
return key in value && typeof Reflect.get(value, key) === "string";
|
|
4898
|
-
}
|
|
4899
|
-
function isEntry(value) {
|
|
4900
|
-
if (typeof value !== "object" || value === null) return false;
|
|
4901
|
-
return hasNumber(value, "index") && hasNumber(value, "startFrame") && hasNumber(value, "endFrame") && hasString(value, "path") && hasNumber(value, "bytes") && hasString(value, "completedAt");
|
|
4902
|
-
}
|
|
4903
|
-
function isManifest(value) {
|
|
4904
|
-
if (typeof value !== "object" || value === null) return false;
|
|
4905
|
-
const completed = Reflect.get(value, "completed");
|
|
4906
|
-
return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
|
|
4907
|
-
}
|
|
4908
|
-
function readSegmentManifest(segmentDir) {
|
|
4909
|
-
const path = join11(segmentDir, MANIFEST_FILENAME);
|
|
4910
|
-
if (!existsSync7(path)) return null;
|
|
4911
|
-
try {
|
|
4912
|
-
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
4913
|
-
return isManifest(parsed) ? parsed : null;
|
|
4914
|
-
} catch {
|
|
4915
|
-
return null;
|
|
4916
|
-
}
|
|
4917
|
-
}
|
|
4918
|
-
function writeSegmentManifest(segmentDir, manifest) {
|
|
4919
|
-
mkdirSync4(segmentDir, { recursive: true });
|
|
4920
|
-
const tmp = join11(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
|
|
4921
|
-
writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
|
|
4922
|
-
renameWithRetry2(tmp, join11(segmentDir, MANIFEST_FILENAME));
|
|
4923
|
-
}
|
|
4924
|
-
async function probeSegmentFrameCount(path, probe) {
|
|
4925
|
-
try {
|
|
4926
|
-
const meta = await probe(path);
|
|
4927
|
-
if (!Number.isFinite(meta.videoStreamDurationSeconds) || !Number.isFinite(meta.fps))
|
|
4928
|
-
return null;
|
|
4929
|
-
if (meta.fps <= 0) return null;
|
|
4930
|
-
return Math.round(meta.videoStreamDurationSeconds * meta.fps);
|
|
4931
|
-
} catch {
|
|
4932
|
-
return null;
|
|
4933
|
-
}
|
|
4934
|
-
}
|
|
4935
|
-
async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
|
|
4936
|
-
const ok = /* @__PURE__ */ new Set();
|
|
4937
|
-
if (manifest.planHash !== expectedHash) return ok;
|
|
4938
|
-
for (const entry of manifest.completed) {
|
|
4939
|
-
if (!existsSync7(entry.path)) continue;
|
|
4940
|
-
const size = statSync4(entry.path).size;
|
|
4941
|
-
if (size <= 0 || size !== entry.bytes) continue;
|
|
4942
|
-
const frames = await probeFrames(entry.path);
|
|
4943
|
-
if (frames !== entry.endFrame - entry.startFrame) continue;
|
|
4944
|
-
ok.add(entry.index);
|
|
4945
|
-
}
|
|
4946
|
-
return ok;
|
|
4947
|
-
}
|
|
4948
|
-
|
|
4949
|
-
// src/services/render/hdrMode.ts
|
|
4950
|
-
import {
|
|
4951
|
-
analyzeCompositionHdr,
|
|
4952
|
-
formatHdrAutoPromotionWarning,
|
|
4953
|
-
gradingKeepsSdr,
|
|
4954
|
-
HDR_AUTO_PROMOTION_PIPELINE,
|
|
4955
|
-
isHdrColorSpace,
|
|
4956
|
-
sanitizeHdrAutoPromotionAsset
|
|
4957
|
-
} from "@hyperframes/engine";
|
|
4958
|
-
function formatDowngradeWarning(outputFormat, forcedHdrWithoutSources) {
|
|
4959
|
-
const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
|
|
4960
|
-
const formatReason = outputFormat === "hls" ? "HLS output is SDR-only (H.264 in MPEG-TS)" : "HDR + alpha is not supported";
|
|
4961
|
-
return `[Render] ${hdrSourceReason}, but format is "${outputFormat}", so falling back to SDR. ${formatReason}. Use --format mp4 for HDR10 output.`;
|
|
4962
|
-
}
|
|
4963
|
-
function findRenderHdrAutoPromotionTrigger(input) {
|
|
4964
|
-
for (const extracted of input.extractionResult?.extracted ?? []) {
|
|
4965
|
-
if (!isHdrColorSpace(extracted.metadata.colorSpace)) continue;
|
|
4966
|
-
const source = input.videos.find(({ id }) => id === extracted.videoId)?.src;
|
|
4967
|
-
if (source) return source;
|
|
4968
|
-
}
|
|
4969
|
-
return input.images.find(({ id }) => input.nativeHdrImageIds.has(id))?.src;
|
|
4970
|
-
}
|
|
4971
|
-
function logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource) {
|
|
4972
|
-
const keptSdr = hasHdrSource && gradingKeepsSdr(hdrMode, input.media);
|
|
4973
|
-
if (keptSdr) {
|
|
4974
|
-
input.log.warn(
|
|
4975
|
-
"[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)."
|
|
4976
|
-
);
|
|
4977
|
-
} else if (effectiveHdr && [...input.media.videos, ...input.media.images].some((m) => m.graded)) {
|
|
4978
|
-
input.log.warn(
|
|
4979
|
-
"[Render] HDR output with graded media: color grading is SDR-only, so graded elements are graded in SDR and placed in the HDR output."
|
|
4980
|
-
);
|
|
4981
|
-
}
|
|
4982
|
-
return keptSdr;
|
|
4983
|
-
}
|
|
4984
|
-
function logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, hasHdrSource) {
|
|
4985
|
-
const keptSdrForGrading = logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource);
|
|
4986
|
-
if (forcedHdrWithoutSources) {
|
|
4987
|
-
input.log.warn(
|
|
4988
|
-
"[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."
|
|
4989
|
-
);
|
|
4990
|
-
}
|
|
4991
|
-
if (!effectiveHdr) {
|
|
4992
|
-
if (keptSdrForGrading) return;
|
|
4993
|
-
input.log.info(
|
|
4994
|
-
hdrMode === "force-sdr" ? "[Render] SDR forced by --sdr flag" : "[Render] No HDR sources detected, rendering SDR"
|
|
4995
|
-
);
|
|
4996
|
-
return;
|
|
4997
|
-
}
|
|
4998
|
-
if (hdrMode === "auto") {
|
|
4999
|
-
input.log.warn(
|
|
5000
|
-
formatHdrAutoPromotionWarning({
|
|
5001
|
-
triggeringAsset: sanitizeHdrAutoPromotionAsset(
|
|
5002
|
-
input.autoPromotionTrigger ?? "an HDR-tagged asset"
|
|
5003
|
-
),
|
|
5004
|
-
output: HDR_AUTO_PROMOTION_PIPELINE
|
|
5005
|
-
})
|
|
5006
|
-
);
|
|
5007
|
-
return;
|
|
5008
|
-
}
|
|
5009
|
-
const reason = forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected, defaulting to HLG)" : "forced by --hdr flag";
|
|
5010
|
-
input.log.info(
|
|
5011
|
-
`[Render] HDR ${reason}, output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
5012
|
-
);
|
|
5013
|
-
}
|
|
5014
|
-
function formatCarriesHdr(format) {
|
|
5015
|
-
return format === "mp4";
|
|
5016
|
-
}
|
|
5017
|
-
function rendersSdrOnly(config, media) {
|
|
5018
|
-
return config.hdrMode === "force-sdr" || gradingKeepsSdr(config.hdrMode, media) || !formatCarriesHdr(config.format ?? "mp4");
|
|
5019
|
-
}
|
|
5020
|
-
function resolveEffectiveHdrMode(input) {
|
|
5021
|
-
const hdrMode = input.hdrMode ?? "auto";
|
|
5022
|
-
const videoColorSpaces = (input.extractionResult?.extracted ?? []).map(
|
|
5023
|
-
(ext) => ext.metadata.colorSpace
|
|
5024
|
-
);
|
|
5025
|
-
const allColorSpaces = [...videoColorSpaces, ...input.imageColorSpaces];
|
|
5026
|
-
const info = allColorSpaces.length > 0 ? analyzeCompositionHdr(allColorSpaces) : null;
|
|
5027
|
-
let effectiveHdr;
|
|
5028
|
-
let forcedHdrWithoutSources = false;
|
|
5029
|
-
if (hdrMode === "force-sdr" || gradingKeepsSdr(hdrMode, input.media)) {
|
|
5030
|
-
effectiveHdr = void 0;
|
|
5031
|
-
} else if (hdrMode === "force-hdr") {
|
|
5032
|
-
if (info?.hasHdr && info.dominantTransfer) {
|
|
5033
|
-
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5034
|
-
} else {
|
|
5035
|
-
effectiveHdr = { transfer: "hlg" };
|
|
5036
|
-
forcedHdrWithoutSources = true;
|
|
5037
|
-
}
|
|
5038
|
-
} else if (info?.hasHdr && info.dominantTransfer) {
|
|
5039
|
-
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5040
|
-
}
|
|
5041
|
-
if (effectiveHdr && !formatCarriesHdr(input.outputFormat)) {
|
|
5042
|
-
input.log.warn(formatDowngradeWarning(input.outputFormat, forcedHdrWithoutSources));
|
|
5043
|
-
effectiveHdr = void 0;
|
|
5044
|
-
}
|
|
5045
|
-
logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, info?.hasHdr === true);
|
|
5046
|
-
return effectiveHdr;
|
|
5047
|
-
}
|
|
5048
|
-
|
|
5049
|
-
// src/services/render/captureBeyondViewport.ts
|
|
5050
|
-
function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
5051
|
-
if (videoCount <= 0) return void 0;
|
|
5052
|
-
return true;
|
|
5053
|
-
}
|
|
5054
|
-
|
|
5055
4858
|
// src/services/render/captureCost.ts
|
|
5056
|
-
import { join as
|
|
4859
|
+
import { join as join11 } from "node:path";
|
|
5057
4860
|
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
5058
4861
|
import {
|
|
5059
4862
|
calculateOptimalWorkers,
|
|
@@ -5258,6 +5061,7 @@ async function runCaptureCalibration(input) {
|
|
|
5258
5061
|
let probeSession = input.probeSession;
|
|
5259
5062
|
let forceScreenshot = input.forceScreenshot;
|
|
5260
5063
|
let lastBrowserConsole = [];
|
|
5064
|
+
let calibrationEvaluateTimedOut = false;
|
|
5261
5065
|
const fps = fpsToNumber4(job.config.fps);
|
|
5262
5066
|
const sessionRef = { current: null };
|
|
5263
5067
|
const runOneCalibration = async (sessionDir, sessionCfg) => {
|
|
@@ -5298,11 +5102,12 @@ async function runCaptureCalibration(input) {
|
|
|
5298
5102
|
});
|
|
5299
5103
|
let calibration;
|
|
5300
5104
|
try {
|
|
5301
|
-
calibration = await runOneCalibration(
|
|
5105
|
+
calibration = await runOneCalibration(join11(workDir, "capture-calibration"), calibrationCfg);
|
|
5302
5106
|
} catch (error) {
|
|
5303
5107
|
const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
|
|
5304
5108
|
if (!shouldFallback) {
|
|
5305
5109
|
calibration = createFailedCaptureCalibrationEstimate("calibration-failed");
|
|
5110
|
+
calibrationEvaluateTimedOut = isEvaluateTimeout(error);
|
|
5306
5111
|
log.warn("[Render] Auto-worker calibration failed; using conservative worker budget.", {
|
|
5307
5112
|
protocolTimeout: calibrationCfg.protocolTimeout,
|
|
5308
5113
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -5331,11 +5136,12 @@ async function runCaptureCalibration(input) {
|
|
|
5331
5136
|
const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
|
|
5332
5137
|
try {
|
|
5333
5138
|
calibration = await runOneCalibration(
|
|
5334
|
-
|
|
5139
|
+
join11(workDir, "capture-calibration-screenshot"),
|
|
5335
5140
|
screenshotCfg
|
|
5336
5141
|
);
|
|
5337
5142
|
} catch (fallbackError) {
|
|
5338
5143
|
calibration = createFailedCaptureCalibrationEstimate("calibration-screenshot-failed");
|
|
5144
|
+
calibrationEvaluateTimedOut = isEvaluateTimeout(fallbackError);
|
|
5339
5145
|
log.warn(
|
|
5340
5146
|
"[Render] Screenshot auto-worker calibration failed after BeginFrame fallback; using conservative worker budget.",
|
|
5341
5147
|
{
|
|
@@ -5352,7 +5158,16 @@ async function runCaptureCalibration(input) {
|
|
|
5352
5158
|
});
|
|
5353
5159
|
}
|
|
5354
5160
|
}
|
|
5355
|
-
return {
|
|
5161
|
+
return {
|
|
5162
|
+
calibration,
|
|
5163
|
+
forceScreenshot,
|
|
5164
|
+
probeSession,
|
|
5165
|
+
lastBrowserConsole,
|
|
5166
|
+
calibrationEvaluateTimedOut
|
|
5167
|
+
};
|
|
5168
|
+
}
|
|
5169
|
+
function isEvaluateTimeout(error) {
|
|
5170
|
+
return /Runtime\.evaluate timed out/i.test(normalizeErrorMessage(error));
|
|
5356
5171
|
}
|
|
5357
5172
|
function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
5358
5173
|
const message = normalizeErrorMessage(error);
|
|
@@ -5361,6 +5176,224 @@ function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
|
5361
5176
|
);
|
|
5362
5177
|
}
|
|
5363
5178
|
|
|
5179
|
+
// src/services/render/pageNeverReady.ts
|
|
5180
|
+
var PAGE_NEVER_READY = "PAGE_NEVER_READY";
|
|
5181
|
+
var PageNeverReadyError = class extends Error {
|
|
5182
|
+
code = PAGE_NEVER_READY;
|
|
5183
|
+
// The producer server reads these off the error for the wire envelope.
|
|
5184
|
+
owner = "user";
|
|
5185
|
+
retryable = false;
|
|
5186
|
+
constructor(cause) {
|
|
5187
|
+
super(
|
|
5188
|
+
"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.",
|
|
5189
|
+
{ cause }
|
|
5190
|
+
);
|
|
5191
|
+
this.name = "PageNeverReadyError";
|
|
5192
|
+
}
|
|
5193
|
+
};
|
|
5194
|
+
function checkPageNeverReady(input) {
|
|
5195
|
+
const { calibrationEvaluateTimedOut, framesRendered, error } = input;
|
|
5196
|
+
if (!calibrationEvaluateTimedOut || framesRendered > 0 || !isEvaluateTimeout(error)) {
|
|
5197
|
+
return void 0;
|
|
5198
|
+
}
|
|
5199
|
+
return new PageNeverReadyError(error);
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
// src/services/render/segmentManifest.ts
|
|
5203
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5204
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5205
|
+
import { join as join12 } from "node:path";
|
|
5206
|
+
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
5207
|
+
var MANIFEST_FILENAME = "segments.json";
|
|
5208
|
+
function computeSegmentPlanHash(input) {
|
|
5209
|
+
const ordered = [
|
|
5210
|
+
input.compositionHash,
|
|
5211
|
+
input.cliVersion,
|
|
5212
|
+
String(input.totalFrames),
|
|
5213
|
+
String(input.segmentFrames),
|
|
5214
|
+
`${input.fps.num}/${input.fps.den}`,
|
|
5215
|
+
`${input.width}x${input.height}`,
|
|
5216
|
+
input.codec,
|
|
5217
|
+
input.preset,
|
|
5218
|
+
input.quality === void 0 ? "" : String(input.quality),
|
|
5219
|
+
input.bitrate ?? "",
|
|
5220
|
+
input.pixelFormat ?? "",
|
|
5221
|
+
input.imageFormat,
|
|
5222
|
+
input.useGpu ? "gpu" : "cpu",
|
|
5223
|
+
`${input.outputWidth}x${input.outputHeight}`,
|
|
5224
|
+
input.motionBlur,
|
|
5225
|
+
input.pageMotionBlur ?? "",
|
|
5226
|
+
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
5227
|
+
];
|
|
5228
|
+
const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
|
|
5229
|
+
if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
|
|
5230
|
+
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
5231
|
+
}
|
|
5232
|
+
function segmentDirFor(projectRendersDir, planHash) {
|
|
5233
|
+
return join12(projectRendersDir, ".hf-segments", planHash);
|
|
5234
|
+
}
|
|
5235
|
+
function hasNumber(value, key) {
|
|
5236
|
+
return key in value && typeof Reflect.get(value, key) === "number";
|
|
5237
|
+
}
|
|
5238
|
+
function hasString(value, key) {
|
|
5239
|
+
return key in value && typeof Reflect.get(value, key) === "string";
|
|
5240
|
+
}
|
|
5241
|
+
function isEntry(value) {
|
|
5242
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5243
|
+
return hasNumber(value, "index") && hasNumber(value, "startFrame") && hasNumber(value, "endFrame") && hasString(value, "path") && hasNumber(value, "bytes") && hasString(value, "completedAt");
|
|
5244
|
+
}
|
|
5245
|
+
function isManifest(value) {
|
|
5246
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5247
|
+
const completed = Reflect.get(value, "completed");
|
|
5248
|
+
return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
|
|
5249
|
+
}
|
|
5250
|
+
function readSegmentManifest(segmentDir) {
|
|
5251
|
+
const path = join12(segmentDir, MANIFEST_FILENAME);
|
|
5252
|
+
if (!existsSync7(path)) return null;
|
|
5253
|
+
try {
|
|
5254
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
5255
|
+
return isManifest(parsed) ? parsed : null;
|
|
5256
|
+
} catch {
|
|
5257
|
+
return null;
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
function writeSegmentManifest(segmentDir, manifest) {
|
|
5261
|
+
mkdirSync4(segmentDir, { recursive: true });
|
|
5262
|
+
const tmp = join12(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
|
|
5263
|
+
writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
|
|
5264
|
+
renameWithRetry2(tmp, join12(segmentDir, MANIFEST_FILENAME));
|
|
5265
|
+
}
|
|
5266
|
+
async function probeSegmentFrameCount(path, probe) {
|
|
5267
|
+
try {
|
|
5268
|
+
const meta = await probe(path);
|
|
5269
|
+
if (!Number.isFinite(meta.videoStreamDurationSeconds) || !Number.isFinite(meta.fps))
|
|
5270
|
+
return null;
|
|
5271
|
+
if (meta.fps <= 0) return null;
|
|
5272
|
+
return Math.round(meta.videoStreamDurationSeconds * meta.fps);
|
|
5273
|
+
} catch {
|
|
5274
|
+
return null;
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
|
|
5278
|
+
const ok = /* @__PURE__ */ new Set();
|
|
5279
|
+
if (manifest.planHash !== expectedHash) return ok;
|
|
5280
|
+
for (const entry of manifest.completed) {
|
|
5281
|
+
if (!existsSync7(entry.path)) continue;
|
|
5282
|
+
const size = statSync4(entry.path).size;
|
|
5283
|
+
if (size <= 0 || size !== entry.bytes) continue;
|
|
5284
|
+
const frames = await probeFrames(entry.path);
|
|
5285
|
+
if (frames !== entry.endFrame - entry.startFrame) continue;
|
|
5286
|
+
ok.add(entry.index);
|
|
5287
|
+
}
|
|
5288
|
+
return ok;
|
|
5289
|
+
}
|
|
5290
|
+
|
|
5291
|
+
// src/services/render/hdrMode.ts
|
|
5292
|
+
import {
|
|
5293
|
+
analyzeCompositionHdr,
|
|
5294
|
+
formatHdrAutoPromotionWarning,
|
|
5295
|
+
gradingKeepsSdr,
|
|
5296
|
+
HDR_AUTO_PROMOTION_PIPELINE,
|
|
5297
|
+
isHdrColorSpace,
|
|
5298
|
+
sanitizeHdrAutoPromotionAsset
|
|
5299
|
+
} from "@hyperframes/engine";
|
|
5300
|
+
function formatDowngradeWarning(outputFormat, forcedHdrWithoutSources) {
|
|
5301
|
+
const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
|
|
5302
|
+
const formatReason = outputFormat === "hls" ? "HLS output is SDR-only (H.264 in MPEG-TS)" : "HDR + alpha is not supported";
|
|
5303
|
+
return `[Render] ${hdrSourceReason}, but format is "${outputFormat}", so falling back to SDR. ${formatReason}. Use --format mp4 for HDR10 output.`;
|
|
5304
|
+
}
|
|
5305
|
+
function findRenderHdrAutoPromotionTrigger(input) {
|
|
5306
|
+
for (const extracted of input.extractionResult?.extracted ?? []) {
|
|
5307
|
+
if (!isHdrColorSpace(extracted.metadata.colorSpace)) continue;
|
|
5308
|
+
const source = input.videos.find(({ id }) => id === extracted.videoId)?.src;
|
|
5309
|
+
if (source) return source;
|
|
5310
|
+
}
|
|
5311
|
+
return input.images.find(({ id }) => input.nativeHdrImageIds.has(id))?.src;
|
|
5312
|
+
}
|
|
5313
|
+
function logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource) {
|
|
5314
|
+
const keptSdr = hasHdrSource && gradingKeepsSdr(hdrMode, input.media);
|
|
5315
|
+
if (keptSdr) {
|
|
5316
|
+
input.log.warn(
|
|
5317
|
+
"[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)."
|
|
5318
|
+
);
|
|
5319
|
+
} else if (effectiveHdr && [...input.media.videos, ...input.media.images].some((m) => m.graded)) {
|
|
5320
|
+
input.log.warn(
|
|
5321
|
+
"[Render] HDR output with graded media: color grading is SDR-only, so graded elements are graded in SDR and placed in the HDR output."
|
|
5322
|
+
);
|
|
5323
|
+
}
|
|
5324
|
+
return keptSdr;
|
|
5325
|
+
}
|
|
5326
|
+
function logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, hasHdrSource) {
|
|
5327
|
+
const keptSdrForGrading = logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource);
|
|
5328
|
+
if (forcedHdrWithoutSources) {
|
|
5329
|
+
input.log.warn(
|
|
5330
|
+
"[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."
|
|
5331
|
+
);
|
|
5332
|
+
}
|
|
5333
|
+
if (!effectiveHdr) {
|
|
5334
|
+
if (keptSdrForGrading) return;
|
|
5335
|
+
input.log.info(
|
|
5336
|
+
hdrMode === "force-sdr" ? "[Render] SDR forced by --sdr flag" : "[Render] No HDR sources detected, rendering SDR"
|
|
5337
|
+
);
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
if (hdrMode === "auto") {
|
|
5341
|
+
input.log.warn(
|
|
5342
|
+
formatHdrAutoPromotionWarning({
|
|
5343
|
+
triggeringAsset: sanitizeHdrAutoPromotionAsset(
|
|
5344
|
+
input.autoPromotionTrigger ?? "an HDR-tagged asset"
|
|
5345
|
+
),
|
|
5346
|
+
output: HDR_AUTO_PROMOTION_PIPELINE
|
|
5347
|
+
})
|
|
5348
|
+
);
|
|
5349
|
+
return;
|
|
5350
|
+
}
|
|
5351
|
+
const reason = forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected, defaulting to HLG)" : "forced by --hdr flag";
|
|
5352
|
+
input.log.info(
|
|
5353
|
+
`[Render] HDR ${reason}, output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
5354
|
+
);
|
|
5355
|
+
}
|
|
5356
|
+
function formatCarriesHdr(format) {
|
|
5357
|
+
return format === "mp4";
|
|
5358
|
+
}
|
|
5359
|
+
function rendersSdrOnly(config, media) {
|
|
5360
|
+
return config.hdrMode === "force-sdr" || gradingKeepsSdr(config.hdrMode, media) || !formatCarriesHdr(config.format ?? "mp4");
|
|
5361
|
+
}
|
|
5362
|
+
function resolveEffectiveHdrMode(input) {
|
|
5363
|
+
const hdrMode = input.hdrMode ?? "auto";
|
|
5364
|
+
const videoColorSpaces = (input.extractionResult?.extracted ?? []).map(
|
|
5365
|
+
(ext) => ext.metadata.colorSpace
|
|
5366
|
+
);
|
|
5367
|
+
const allColorSpaces = [...videoColorSpaces, ...input.imageColorSpaces];
|
|
5368
|
+
const info = allColorSpaces.length > 0 ? analyzeCompositionHdr(allColorSpaces) : null;
|
|
5369
|
+
let effectiveHdr;
|
|
5370
|
+
let forcedHdrWithoutSources = false;
|
|
5371
|
+
if (hdrMode === "force-sdr" || gradingKeepsSdr(hdrMode, input.media)) {
|
|
5372
|
+
effectiveHdr = void 0;
|
|
5373
|
+
} else if (hdrMode === "force-hdr") {
|
|
5374
|
+
if (info?.hasHdr && info.dominantTransfer) {
|
|
5375
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5376
|
+
} else {
|
|
5377
|
+
effectiveHdr = { transfer: "hlg" };
|
|
5378
|
+
forcedHdrWithoutSources = true;
|
|
5379
|
+
}
|
|
5380
|
+
} else if (info?.hasHdr && info.dominantTransfer) {
|
|
5381
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5382
|
+
}
|
|
5383
|
+
if (effectiveHdr && !formatCarriesHdr(input.outputFormat)) {
|
|
5384
|
+
input.log.warn(formatDowngradeWarning(input.outputFormat, forcedHdrWithoutSources));
|
|
5385
|
+
effectiveHdr = void 0;
|
|
5386
|
+
}
|
|
5387
|
+
logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, info?.hasHdr === true);
|
|
5388
|
+
return effectiveHdr;
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5391
|
+
// src/services/render/captureBeyondViewport.ts
|
|
5392
|
+
function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
5393
|
+
if (videoCount <= 0) return void 0;
|
|
5394
|
+
return true;
|
|
5395
|
+
}
|
|
5396
|
+
|
|
5364
5397
|
// src/services/render/observability.ts
|
|
5365
5398
|
import { createHash as createHash3 } from "node:crypto";
|
|
5366
5399
|
import { redactTelemetryString } from "@hyperframes/core";
|
|
@@ -9493,17 +9526,24 @@ var VideoExtractionStageError = class extends Error {
|
|
|
9493
9526
|
publicMetadata;
|
|
9494
9527
|
};
|
|
9495
9528
|
function assertVideoExtractionSucceeded(result) {
|
|
9496
|
-
|
|
9529
|
+
throwIfHostFailure(result);
|
|
9497
9530
|
const error = buildVideoExtractionStageError(result);
|
|
9498
9531
|
if (error) throw error;
|
|
9499
9532
|
}
|
|
9500
|
-
function
|
|
9533
|
+
function throwIfHostFailure(result) {
|
|
9501
9534
|
const interrupted = result.errors.find((failure) => failure.kind === "external_interruption");
|
|
9502
|
-
if (
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9535
|
+
if (interrupted) {
|
|
9536
|
+
throw encoderFailureError("Video frame extraction failed", {
|
|
9537
|
+
error: String(interrupted.error),
|
|
9538
|
+
failureReason: "external_interruption"
|
|
9539
|
+
});
|
|
9540
|
+
}
|
|
9541
|
+
const full = result.errors.filter((failure) => failure.kind === "disk_full").length;
|
|
9542
|
+
if (full === 0) return;
|
|
9543
|
+
throw Object.assign(
|
|
9544
|
+
new Error(`ENOSPC: no space left on device, extracting the frames of ${full} video source(s)`),
|
|
9545
|
+
{ code: "ENOSPC" }
|
|
9546
|
+
);
|
|
9507
9547
|
}
|
|
9508
9548
|
function buildVideoExtractionStageError(result) {
|
|
9509
9549
|
if (result.success && result.errors.length === 0) return null;
|
|
@@ -9718,7 +9758,7 @@ async function runExtractVideosStage(input) {
|
|
|
9718
9758
|
);
|
|
9719
9759
|
extractionResult.phaseBreakdown.transientRetries = (extractionResult.phaseBreakdown.transientRetries ?? 0) + hdrProbeTransientRetries;
|
|
9720
9760
|
assertNotAborted();
|
|
9721
|
-
|
|
9761
|
+
throwIfHostFailure(extractionResult);
|
|
9722
9762
|
failureToEnforce = applyVideoExtractionFailurePolicy(extractionResult, extractionPolicy, log);
|
|
9723
9763
|
materializeExtractedFramesForCompiledDir(extractionResult.extracted, compiledDir, {
|
|
9724
9764
|
materializeSymlinks
|
|
@@ -12856,6 +12896,22 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
12856
12896
|
}
|
|
12857
12897
|
const remainingCount = countFrameRanges(remaining);
|
|
12858
12898
|
const madeProgress = captureAttemptMadeProgress(frameCount, remainingCount);
|
|
12899
|
+
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)) {
|
|
12900
|
+
initializationRetriesUsed++;
|
|
12901
|
+
const nextWorkers2 = getNextRetryWorkerCount(currentWorkers);
|
|
12902
|
+
options.log.warn(
|
|
12903
|
+
"[Render] Browser initialization timed out; retrying with fewer workers.",
|
|
12904
|
+
{
|
|
12905
|
+
fromWorkers: currentWorkers,
|
|
12906
|
+
toWorkers: nextWorkers2,
|
|
12907
|
+
error: failure.message
|
|
12908
|
+
}
|
|
12909
|
+
);
|
|
12910
|
+
currentWorkers = nextWorkers2;
|
|
12911
|
+
missingRanges = remaining;
|
|
12912
|
+
attempt++;
|
|
12913
|
+
continue;
|
|
12914
|
+
}
|
|
12859
12915
|
if (options.allowRetry && isTransientCaptureRetryEligible(failure.kind, remaining, transientRetriesUsed)) {
|
|
12860
12916
|
transientRetriesUsed++;
|
|
12861
12917
|
options.log.warn(
|
|
@@ -12873,22 +12929,6 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
12873
12929
|
pendingTransientRetry = true;
|
|
12874
12930
|
continue;
|
|
12875
12931
|
}
|
|
12876
|
-
if (options.allowRetry && !madeProgress && initializationRetriesUsed === 0 && failure.kind === "protocol_timeout" && /\bNetwork\.enable timed out/i.test(failure.message)) {
|
|
12877
|
-
initializationRetriesUsed++;
|
|
12878
|
-
const nextWorkers2 = getNextRetryWorkerCount(currentWorkers);
|
|
12879
|
-
options.log.warn(
|
|
12880
|
-
"[Render] Browser initialization timed out; retrying once with fresh sessions.",
|
|
12881
|
-
{
|
|
12882
|
-
fromWorkers: currentWorkers,
|
|
12883
|
-
toWorkers: nextWorkers2,
|
|
12884
|
-
error: failure.message
|
|
12885
|
-
}
|
|
12886
|
-
);
|
|
12887
|
-
currentWorkers = nextWorkers2;
|
|
12888
|
-
missingRanges = remaining;
|
|
12889
|
-
attempt++;
|
|
12890
|
-
continue;
|
|
12891
|
-
}
|
|
12892
12932
|
if (!madeProgress) {
|
|
12893
12933
|
options.log.warn(
|
|
12894
12934
|
"[Render] Capture attempt made no forward progress; composition is likely structurally broken, not retrying.",
|
|
@@ -13446,6 +13486,7 @@ async function executeRenderPipeline(input) {
|
|
|
13446
13486
|
const onProgress = execution.onProgress;
|
|
13447
13487
|
const tooSlow = new AbortController();
|
|
13448
13488
|
let tooSlowError;
|
|
13489
|
+
let calibrationEvaluateTimedOut = false;
|
|
13449
13490
|
const executionSignal = job.config.maxRenderMs ? AbortSignal.any([tooSlow.signal, ...execution.signal ? [execution.signal] : []]) : execution.signal;
|
|
13450
13491
|
let fileServer = null;
|
|
13451
13492
|
let probeSession = null;
|
|
@@ -14188,6 +14229,7 @@ async function executeRenderPipeline(input) {
|
|
|
14188
14229
|
{ heartbeatMessage: "browser calibrating (frames not started)" }
|
|
14189
14230
|
);
|
|
14190
14231
|
captureCalibration = outcome.calibration;
|
|
14232
|
+
calibrationEvaluateTimedOut = outcome.calibrationEvaluateTimedOut;
|
|
14191
14233
|
captureForceScreenshot = outcome.forceScreenshot;
|
|
14192
14234
|
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
|
14193
14235
|
probeSession = outcome.probeSession;
|
|
@@ -15338,7 +15380,12 @@ async function executeRenderPipeline(input) {
|
|
|
15338
15380
|
await eventPublisher.flush();
|
|
15339
15381
|
} catch (caught) {
|
|
15340
15382
|
await pendingAudio;
|
|
15341
|
-
const
|
|
15383
|
+
const pageNeverReady = checkPageNeverReady({
|
|
15384
|
+
calibrationEvaluateTimedOut,
|
|
15385
|
+
framesRendered: job.framesRendered ?? 0,
|
|
15386
|
+
error: caught
|
|
15387
|
+
});
|
|
15388
|
+
const error = surfaceScriptFailure(tooSlowError ?? pageNeverReady ?? caught, job);
|
|
15342
15389
|
if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
|
|
15343
15390
|
job.error = error instanceof Error ? error.message : "render_cancelled";
|
|
15344
15391
|
updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
|
|
@@ -19801,7 +19848,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
19801
19848
|
"VIDEO_EXTRACTION_FAILED",
|
|
19802
19849
|
"ENCODER_INTERRUPTED",
|
|
19803
19850
|
"RENDER_TOO_SLOW",
|
|
19804
|
-
"RENDER_STAGE_STALLED"
|
|
19851
|
+
"RENDER_STAGE_STALLED",
|
|
19852
|
+
"PAGE_NEVER_READY"
|
|
19805
19853
|
]);
|
|
19806
19854
|
function extractSafeRenderErrorCode(error) {
|
|
19807
19855
|
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|