@hyperframes/producer 0.9.10 → 0.9.12
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 +25 -25
- package/dist/index.js +244 -201
- package/dist/public-server.js +244 -201
- 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/distributed.js
CHANGED
|
@@ -1463,31 +1463,6 @@ function resolveCaptureStallTimeoutMs() {
|
|
|
1463
1463
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CAPTURE_STALL_MS;
|
|
1464
1464
|
}
|
|
1465
1465
|
|
|
1466
|
-
// src/services/render/segmentManifest.ts
|
|
1467
|
-
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
1468
|
-
|
|
1469
|
-
// src/services/render/hdrMode.ts
|
|
1470
|
-
import {
|
|
1471
|
-
analyzeCompositionHdr,
|
|
1472
|
-
formatHdrAutoPromotionWarning,
|
|
1473
|
-
gradingKeepsSdr,
|
|
1474
|
-
HDR_AUTO_PROMOTION_PIPELINE,
|
|
1475
|
-
isHdrColorSpace,
|
|
1476
|
-
sanitizeHdrAutoPromotionAsset
|
|
1477
|
-
} from "@hyperframes/engine";
|
|
1478
|
-
function formatCarriesHdr(format) {
|
|
1479
|
-
return format === "mp4";
|
|
1480
|
-
}
|
|
1481
|
-
function rendersSdrOnly(config, media) {
|
|
1482
|
-
return config.hdrMode === "force-sdr" || gradingKeepsSdr(config.hdrMode, media) || !formatCarriesHdr(config.format ?? "mp4");
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
// src/services/render/captureBeyondViewport.ts
|
|
1486
|
-
function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
1487
|
-
if (videoCount <= 0) return void 0;
|
|
1488
|
-
return true;
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
1466
|
// src/services/render/captureCost.ts
|
|
1492
1467
|
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
1493
1468
|
import {
|
|
@@ -1574,6 +1549,31 @@ async function measureCaptureCostFromSession(session, totalFrames, fps, log) {
|
|
|
1574
1549
|
};
|
|
1575
1550
|
}
|
|
1576
1551
|
|
|
1552
|
+
// src/services/render/segmentManifest.ts
|
|
1553
|
+
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
1554
|
+
|
|
1555
|
+
// src/services/render/hdrMode.ts
|
|
1556
|
+
import {
|
|
1557
|
+
analyzeCompositionHdr,
|
|
1558
|
+
formatHdrAutoPromotionWarning,
|
|
1559
|
+
gradingKeepsSdr,
|
|
1560
|
+
HDR_AUTO_PROMOTION_PIPELINE,
|
|
1561
|
+
isHdrColorSpace,
|
|
1562
|
+
sanitizeHdrAutoPromotionAsset
|
|
1563
|
+
} from "@hyperframes/engine";
|
|
1564
|
+
function formatCarriesHdr(format) {
|
|
1565
|
+
return format === "mp4";
|
|
1566
|
+
}
|
|
1567
|
+
function rendersSdrOnly(config, media) {
|
|
1568
|
+
return config.hdrMode === "force-sdr" || gradingKeepsSdr(config.hdrMode, media) || !formatCarriesHdr(config.format ?? "mp4");
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/services/render/captureBeyondViewport.ts
|
|
1572
|
+
function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
1573
|
+
if (videoCount <= 0) return void 0;
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
1577
|
// src/services/render/observability.ts
|
|
1578
1578
|
import { redactTelemetryString } from "@hyperframes/core";
|
|
1579
1579
|
import { CaptureFailure as CaptureFailure2 } from "@hyperframes/engine";
|
package/dist/index.js
CHANGED
|
@@ -4857,203 +4857,8 @@ function startRenderEtaGuard(options) {
|
|
|
4857
4857
|
return stop;
|
|
4858
4858
|
}
|
|
4859
4859
|
|
|
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
4860
|
// src/services/render/captureCost.ts
|
|
5056
|
-
import { join as
|
|
4861
|
+
import { join as join11 } from "node:path";
|
|
5057
4862
|
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
5058
4863
|
import {
|
|
5059
4864
|
calculateOptimalWorkers,
|
|
@@ -5258,6 +5063,7 @@ async function runCaptureCalibration(input) {
|
|
|
5258
5063
|
let probeSession = input.probeSession;
|
|
5259
5064
|
let forceScreenshot = input.forceScreenshot;
|
|
5260
5065
|
let lastBrowserConsole = [];
|
|
5066
|
+
let calibrationEvaluateTimedOut = false;
|
|
5261
5067
|
const fps = fpsToNumber4(job.config.fps);
|
|
5262
5068
|
const sessionRef = { current: null };
|
|
5263
5069
|
const runOneCalibration = async (sessionDir, sessionCfg) => {
|
|
@@ -5298,11 +5104,12 @@ async function runCaptureCalibration(input) {
|
|
|
5298
5104
|
});
|
|
5299
5105
|
let calibration;
|
|
5300
5106
|
try {
|
|
5301
|
-
calibration = await runOneCalibration(
|
|
5107
|
+
calibration = await runOneCalibration(join11(workDir, "capture-calibration"), calibrationCfg);
|
|
5302
5108
|
} catch (error) {
|
|
5303
5109
|
const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
|
|
5304
5110
|
if (!shouldFallback) {
|
|
5305
5111
|
calibration = createFailedCaptureCalibrationEstimate("calibration-failed");
|
|
5112
|
+
calibrationEvaluateTimedOut = isEvaluateTimeout(error);
|
|
5306
5113
|
log.warn("[Render] Auto-worker calibration failed; using conservative worker budget.", {
|
|
5307
5114
|
protocolTimeout: calibrationCfg.protocolTimeout,
|
|
5308
5115
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -5331,11 +5138,12 @@ async function runCaptureCalibration(input) {
|
|
|
5331
5138
|
const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
|
|
5332
5139
|
try {
|
|
5333
5140
|
calibration = await runOneCalibration(
|
|
5334
|
-
|
|
5141
|
+
join11(workDir, "capture-calibration-screenshot"),
|
|
5335
5142
|
screenshotCfg
|
|
5336
5143
|
);
|
|
5337
5144
|
} catch (fallbackError) {
|
|
5338
5145
|
calibration = createFailedCaptureCalibrationEstimate("calibration-screenshot-failed");
|
|
5146
|
+
calibrationEvaluateTimedOut = isEvaluateTimeout(fallbackError);
|
|
5339
5147
|
log.warn(
|
|
5340
5148
|
"[Render] Screenshot auto-worker calibration failed after BeginFrame fallback; using conservative worker budget.",
|
|
5341
5149
|
{
|
|
@@ -5352,7 +5160,16 @@ async function runCaptureCalibration(input) {
|
|
|
5352
5160
|
});
|
|
5353
5161
|
}
|
|
5354
5162
|
}
|
|
5355
|
-
return {
|
|
5163
|
+
return {
|
|
5164
|
+
calibration,
|
|
5165
|
+
forceScreenshot,
|
|
5166
|
+
probeSession,
|
|
5167
|
+
lastBrowserConsole,
|
|
5168
|
+
calibrationEvaluateTimedOut
|
|
5169
|
+
};
|
|
5170
|
+
}
|
|
5171
|
+
function isEvaluateTimeout(error) {
|
|
5172
|
+
return /Runtime\.evaluate timed out/i.test(normalizeErrorMessage(error));
|
|
5356
5173
|
}
|
|
5357
5174
|
function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
5358
5175
|
const message = normalizeErrorMessage(error);
|
|
@@ -5361,6 +5178,224 @@ function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
|
5361
5178
|
);
|
|
5362
5179
|
}
|
|
5363
5180
|
|
|
5181
|
+
// src/services/render/pageNeverReady.ts
|
|
5182
|
+
var PAGE_NEVER_READY = "PAGE_NEVER_READY";
|
|
5183
|
+
var PageNeverReadyError = class extends Error {
|
|
5184
|
+
code = PAGE_NEVER_READY;
|
|
5185
|
+
// The producer server reads these off the error for the wire envelope.
|
|
5186
|
+
owner = "user";
|
|
5187
|
+
retryable = false;
|
|
5188
|
+
constructor(cause) {
|
|
5189
|
+
super(
|
|
5190
|
+
"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.",
|
|
5191
|
+
{ cause }
|
|
5192
|
+
);
|
|
5193
|
+
this.name = "PageNeverReadyError";
|
|
5194
|
+
}
|
|
5195
|
+
};
|
|
5196
|
+
function checkPageNeverReady(input) {
|
|
5197
|
+
const { calibrationEvaluateTimedOut, framesRendered, error } = input;
|
|
5198
|
+
if (!calibrationEvaluateTimedOut || framesRendered > 0 || !isEvaluateTimeout(error)) {
|
|
5199
|
+
return void 0;
|
|
5200
|
+
}
|
|
5201
|
+
return new PageNeverReadyError(error);
|
|
5202
|
+
}
|
|
5203
|
+
|
|
5204
|
+
// src/services/render/segmentManifest.ts
|
|
5205
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5206
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5207
|
+
import { join as join12 } from "node:path";
|
|
5208
|
+
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
5209
|
+
var MANIFEST_FILENAME = "segments.json";
|
|
5210
|
+
function computeSegmentPlanHash(input) {
|
|
5211
|
+
const ordered = [
|
|
5212
|
+
input.compositionHash,
|
|
5213
|
+
input.cliVersion,
|
|
5214
|
+
String(input.totalFrames),
|
|
5215
|
+
String(input.segmentFrames),
|
|
5216
|
+
`${input.fps.num}/${input.fps.den}`,
|
|
5217
|
+
`${input.width}x${input.height}`,
|
|
5218
|
+
input.codec,
|
|
5219
|
+
input.preset,
|
|
5220
|
+
input.quality === void 0 ? "" : String(input.quality),
|
|
5221
|
+
input.bitrate ?? "",
|
|
5222
|
+
input.pixelFormat ?? "",
|
|
5223
|
+
input.imageFormat,
|
|
5224
|
+
input.useGpu ? "gpu" : "cpu",
|
|
5225
|
+
`${input.outputWidth}x${input.outputHeight}`,
|
|
5226
|
+
input.motionBlur,
|
|
5227
|
+
input.pageMotionBlur ?? "",
|
|
5228
|
+
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
5229
|
+
];
|
|
5230
|
+
const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
|
|
5231
|
+
if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
|
|
5232
|
+
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
5233
|
+
}
|
|
5234
|
+
function segmentDirFor(projectRendersDir, planHash) {
|
|
5235
|
+
return join12(projectRendersDir, ".hf-segments", planHash);
|
|
5236
|
+
}
|
|
5237
|
+
function hasNumber(value, key) {
|
|
5238
|
+
return key in value && typeof Reflect.get(value, key) === "number";
|
|
5239
|
+
}
|
|
5240
|
+
function hasString(value, key) {
|
|
5241
|
+
return key in value && typeof Reflect.get(value, key) === "string";
|
|
5242
|
+
}
|
|
5243
|
+
function isEntry(value) {
|
|
5244
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5245
|
+
return hasNumber(value, "index") && hasNumber(value, "startFrame") && hasNumber(value, "endFrame") && hasString(value, "path") && hasNumber(value, "bytes") && hasString(value, "completedAt");
|
|
5246
|
+
}
|
|
5247
|
+
function isManifest(value) {
|
|
5248
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5249
|
+
const completed = Reflect.get(value, "completed");
|
|
5250
|
+
return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
|
|
5251
|
+
}
|
|
5252
|
+
function readSegmentManifest(segmentDir) {
|
|
5253
|
+
const path = join12(segmentDir, MANIFEST_FILENAME);
|
|
5254
|
+
if (!existsSync7(path)) return null;
|
|
5255
|
+
try {
|
|
5256
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
5257
|
+
return isManifest(parsed) ? parsed : null;
|
|
5258
|
+
} catch {
|
|
5259
|
+
return null;
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
function writeSegmentManifest(segmentDir, manifest) {
|
|
5263
|
+
mkdirSync4(segmentDir, { recursive: true });
|
|
5264
|
+
const tmp = join12(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
|
|
5265
|
+
writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
|
|
5266
|
+
renameWithRetry2(tmp, join12(segmentDir, MANIFEST_FILENAME));
|
|
5267
|
+
}
|
|
5268
|
+
async function probeSegmentFrameCount(path, probe) {
|
|
5269
|
+
try {
|
|
5270
|
+
const meta = await probe(path);
|
|
5271
|
+
if (!Number.isFinite(meta.videoStreamDurationSeconds) || !Number.isFinite(meta.fps))
|
|
5272
|
+
return null;
|
|
5273
|
+
if (meta.fps <= 0) return null;
|
|
5274
|
+
return Math.round(meta.videoStreamDurationSeconds * meta.fps);
|
|
5275
|
+
} catch {
|
|
5276
|
+
return null;
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
|
|
5280
|
+
const ok = /* @__PURE__ */ new Set();
|
|
5281
|
+
if (manifest.planHash !== expectedHash) return ok;
|
|
5282
|
+
for (const entry of manifest.completed) {
|
|
5283
|
+
if (!existsSync7(entry.path)) continue;
|
|
5284
|
+
const size = statSync4(entry.path).size;
|
|
5285
|
+
if (size <= 0 || size !== entry.bytes) continue;
|
|
5286
|
+
const frames = await probeFrames(entry.path);
|
|
5287
|
+
if (frames !== entry.endFrame - entry.startFrame) continue;
|
|
5288
|
+
ok.add(entry.index);
|
|
5289
|
+
}
|
|
5290
|
+
return ok;
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
// src/services/render/hdrMode.ts
|
|
5294
|
+
import {
|
|
5295
|
+
analyzeCompositionHdr,
|
|
5296
|
+
formatHdrAutoPromotionWarning,
|
|
5297
|
+
gradingKeepsSdr,
|
|
5298
|
+
HDR_AUTO_PROMOTION_PIPELINE,
|
|
5299
|
+
isHdrColorSpace,
|
|
5300
|
+
sanitizeHdrAutoPromotionAsset
|
|
5301
|
+
} from "@hyperframes/engine";
|
|
5302
|
+
function formatDowngradeWarning(outputFormat, forcedHdrWithoutSources) {
|
|
5303
|
+
const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
|
|
5304
|
+
const formatReason = outputFormat === "hls" ? "HLS output is SDR-only (H.264 in MPEG-TS)" : "HDR + alpha is not supported";
|
|
5305
|
+
return `[Render] ${hdrSourceReason}, but format is "${outputFormat}", so falling back to SDR. ${formatReason}. Use --format mp4 for HDR10 output.`;
|
|
5306
|
+
}
|
|
5307
|
+
function findRenderHdrAutoPromotionTrigger(input) {
|
|
5308
|
+
for (const extracted of input.extractionResult?.extracted ?? []) {
|
|
5309
|
+
if (!isHdrColorSpace(extracted.metadata.colorSpace)) continue;
|
|
5310
|
+
const source = input.videos.find(({ id }) => id === extracted.videoId)?.src;
|
|
5311
|
+
if (source) return source;
|
|
5312
|
+
}
|
|
5313
|
+
return input.images.find(({ id }) => input.nativeHdrImageIds.has(id))?.src;
|
|
5314
|
+
}
|
|
5315
|
+
function logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource) {
|
|
5316
|
+
const keptSdr = hasHdrSource && gradingKeepsSdr(hdrMode, input.media);
|
|
5317
|
+
if (keptSdr) {
|
|
5318
|
+
input.log.warn(
|
|
5319
|
+
"[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)."
|
|
5320
|
+
);
|
|
5321
|
+
} else if (effectiveHdr && [...input.media.videos, ...input.media.images].some((m) => m.graded)) {
|
|
5322
|
+
input.log.warn(
|
|
5323
|
+
"[Render] HDR output with graded media: color grading is SDR-only, so graded elements are graded in SDR and placed in the HDR output."
|
|
5324
|
+
);
|
|
5325
|
+
}
|
|
5326
|
+
return keptSdr;
|
|
5327
|
+
}
|
|
5328
|
+
function logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, hasHdrSource) {
|
|
5329
|
+
const keptSdrForGrading = logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource);
|
|
5330
|
+
if (forcedHdrWithoutSources) {
|
|
5331
|
+
input.log.warn(
|
|
5332
|
+
"[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."
|
|
5333
|
+
);
|
|
5334
|
+
}
|
|
5335
|
+
if (!effectiveHdr) {
|
|
5336
|
+
if (keptSdrForGrading) return;
|
|
5337
|
+
input.log.info(
|
|
5338
|
+
hdrMode === "force-sdr" ? "[Render] SDR forced by --sdr flag" : "[Render] No HDR sources detected, rendering SDR"
|
|
5339
|
+
);
|
|
5340
|
+
return;
|
|
5341
|
+
}
|
|
5342
|
+
if (hdrMode === "auto") {
|
|
5343
|
+
input.log.warn(
|
|
5344
|
+
formatHdrAutoPromotionWarning({
|
|
5345
|
+
triggeringAsset: sanitizeHdrAutoPromotionAsset(
|
|
5346
|
+
input.autoPromotionTrigger ?? "an HDR-tagged asset"
|
|
5347
|
+
),
|
|
5348
|
+
output: HDR_AUTO_PROMOTION_PIPELINE
|
|
5349
|
+
})
|
|
5350
|
+
);
|
|
5351
|
+
return;
|
|
5352
|
+
}
|
|
5353
|
+
const reason = forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected, defaulting to HLG)" : "forced by --hdr flag";
|
|
5354
|
+
input.log.info(
|
|
5355
|
+
`[Render] HDR ${reason}, output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
5356
|
+
);
|
|
5357
|
+
}
|
|
5358
|
+
function formatCarriesHdr(format) {
|
|
5359
|
+
return format === "mp4";
|
|
5360
|
+
}
|
|
5361
|
+
function rendersSdrOnly(config, media) {
|
|
5362
|
+
return config.hdrMode === "force-sdr" || gradingKeepsSdr(config.hdrMode, media) || !formatCarriesHdr(config.format ?? "mp4");
|
|
5363
|
+
}
|
|
5364
|
+
function resolveEffectiveHdrMode(input) {
|
|
5365
|
+
const hdrMode = input.hdrMode ?? "auto";
|
|
5366
|
+
const videoColorSpaces = (input.extractionResult?.extracted ?? []).map(
|
|
5367
|
+
(ext) => ext.metadata.colorSpace
|
|
5368
|
+
);
|
|
5369
|
+
const allColorSpaces = [...videoColorSpaces, ...input.imageColorSpaces];
|
|
5370
|
+
const info = allColorSpaces.length > 0 ? analyzeCompositionHdr(allColorSpaces) : null;
|
|
5371
|
+
let effectiveHdr;
|
|
5372
|
+
let forcedHdrWithoutSources = false;
|
|
5373
|
+
if (hdrMode === "force-sdr" || gradingKeepsSdr(hdrMode, input.media)) {
|
|
5374
|
+
effectiveHdr = void 0;
|
|
5375
|
+
} else if (hdrMode === "force-hdr") {
|
|
5376
|
+
if (info?.hasHdr && info.dominantTransfer) {
|
|
5377
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5378
|
+
} else {
|
|
5379
|
+
effectiveHdr = { transfer: "hlg" };
|
|
5380
|
+
forcedHdrWithoutSources = true;
|
|
5381
|
+
}
|
|
5382
|
+
} else if (info?.hasHdr && info.dominantTransfer) {
|
|
5383
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5384
|
+
}
|
|
5385
|
+
if (effectiveHdr && !formatCarriesHdr(input.outputFormat)) {
|
|
5386
|
+
input.log.warn(formatDowngradeWarning(input.outputFormat, forcedHdrWithoutSources));
|
|
5387
|
+
effectiveHdr = void 0;
|
|
5388
|
+
}
|
|
5389
|
+
logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, info?.hasHdr === true);
|
|
5390
|
+
return effectiveHdr;
|
|
5391
|
+
}
|
|
5392
|
+
|
|
5393
|
+
// src/services/render/captureBeyondViewport.ts
|
|
5394
|
+
function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
5395
|
+
if (videoCount <= 0) return void 0;
|
|
5396
|
+
return true;
|
|
5397
|
+
}
|
|
5398
|
+
|
|
5364
5399
|
// src/services/render/observability.ts
|
|
5365
5400
|
import { createHash as createHash3 } from "node:crypto";
|
|
5366
5401
|
import { redactTelemetryString } from "@hyperframes/core";
|
|
@@ -13446,6 +13481,7 @@ async function executeRenderPipeline(input) {
|
|
|
13446
13481
|
const onProgress = execution.onProgress;
|
|
13447
13482
|
const tooSlow = new AbortController();
|
|
13448
13483
|
let tooSlowError;
|
|
13484
|
+
let calibrationEvaluateTimedOut = false;
|
|
13449
13485
|
const executionSignal = job.config.maxRenderMs ? AbortSignal.any([tooSlow.signal, ...execution.signal ? [execution.signal] : []]) : execution.signal;
|
|
13450
13486
|
let fileServer = null;
|
|
13451
13487
|
let probeSession = null;
|
|
@@ -14188,6 +14224,7 @@ async function executeRenderPipeline(input) {
|
|
|
14188
14224
|
{ heartbeatMessage: "browser calibrating (frames not started)" }
|
|
14189
14225
|
);
|
|
14190
14226
|
captureCalibration = outcome.calibration;
|
|
14227
|
+
calibrationEvaluateTimedOut = outcome.calibrationEvaluateTimedOut;
|
|
14191
14228
|
captureForceScreenshot = outcome.forceScreenshot;
|
|
14192
14229
|
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
|
14193
14230
|
probeSession = outcome.probeSession;
|
|
@@ -15338,7 +15375,12 @@ async function executeRenderPipeline(input) {
|
|
|
15338
15375
|
await eventPublisher.flush();
|
|
15339
15376
|
} catch (caught) {
|
|
15340
15377
|
await pendingAudio;
|
|
15341
|
-
const
|
|
15378
|
+
const pageNeverReady = checkPageNeverReady({
|
|
15379
|
+
calibrationEvaluateTimedOut,
|
|
15380
|
+
framesRendered: job.framesRendered ?? 0,
|
|
15381
|
+
error: caught
|
|
15382
|
+
});
|
|
15383
|
+
const error = surfaceScriptFailure(tooSlowError ?? pageNeverReady ?? caught, job);
|
|
15342
15384
|
if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
|
|
15343
15385
|
job.error = error instanceof Error ? error.message : "render_cancelled";
|
|
15344
15386
|
updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
|
|
@@ -19801,7 +19843,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
19801
19843
|
"VIDEO_EXTRACTION_FAILED",
|
|
19802
19844
|
"ENCODER_INTERRUPTED",
|
|
19803
19845
|
"RENDER_TOO_SLOW",
|
|
19804
|
-
"RENDER_STAGE_STALLED"
|
|
19846
|
+
"RENDER_STAGE_STALLED",
|
|
19847
|
+
"PAGE_NEVER_READY"
|
|
19805
19848
|
]);
|
|
19806
19849
|
function extractSafeRenderErrorCode(error) {
|
|
19807
19850
|
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
package/dist/public-server.js
CHANGED
|
@@ -4870,203 +4870,8 @@ function startRenderEtaGuard(options) {
|
|
|
4870
4870
|
return stop;
|
|
4871
4871
|
}
|
|
4872
4872
|
|
|
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
4873
|
// src/services/render/captureCost.ts
|
|
5069
|
-
import { join as
|
|
4874
|
+
import { join as join11 } from "node:path";
|
|
5070
4875
|
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
5071
4876
|
import {
|
|
5072
4877
|
calculateOptimalWorkers,
|
|
@@ -5271,6 +5076,7 @@ async function runCaptureCalibration(input) {
|
|
|
5271
5076
|
let probeSession = input.probeSession;
|
|
5272
5077
|
let forceScreenshot = input.forceScreenshot;
|
|
5273
5078
|
let lastBrowserConsole = [];
|
|
5079
|
+
let calibrationEvaluateTimedOut = false;
|
|
5274
5080
|
const fps = fpsToNumber4(job.config.fps);
|
|
5275
5081
|
const sessionRef = { current: null };
|
|
5276
5082
|
const runOneCalibration = async (sessionDir, sessionCfg) => {
|
|
@@ -5311,11 +5117,12 @@ async function runCaptureCalibration(input) {
|
|
|
5311
5117
|
});
|
|
5312
5118
|
let calibration;
|
|
5313
5119
|
try {
|
|
5314
|
-
calibration = await runOneCalibration(
|
|
5120
|
+
calibration = await runOneCalibration(join11(workDir, "capture-calibration"), calibrationCfg);
|
|
5315
5121
|
} catch (error) {
|
|
5316
5122
|
const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
|
|
5317
5123
|
if (!shouldFallback) {
|
|
5318
5124
|
calibration = createFailedCaptureCalibrationEstimate("calibration-failed");
|
|
5125
|
+
calibrationEvaluateTimedOut = isEvaluateTimeout(error);
|
|
5319
5126
|
log.warn("[Render] Auto-worker calibration failed; using conservative worker budget.", {
|
|
5320
5127
|
protocolTimeout: calibrationCfg.protocolTimeout,
|
|
5321
5128
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -5344,11 +5151,12 @@ async function runCaptureCalibration(input) {
|
|
|
5344
5151
|
const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
|
|
5345
5152
|
try {
|
|
5346
5153
|
calibration = await runOneCalibration(
|
|
5347
|
-
|
|
5154
|
+
join11(workDir, "capture-calibration-screenshot"),
|
|
5348
5155
|
screenshotCfg
|
|
5349
5156
|
);
|
|
5350
5157
|
} catch (fallbackError) {
|
|
5351
5158
|
calibration = createFailedCaptureCalibrationEstimate("calibration-screenshot-failed");
|
|
5159
|
+
calibrationEvaluateTimedOut = isEvaluateTimeout(fallbackError);
|
|
5352
5160
|
log.warn(
|
|
5353
5161
|
"[Render] Screenshot auto-worker calibration failed after BeginFrame fallback; using conservative worker budget.",
|
|
5354
5162
|
{
|
|
@@ -5365,7 +5173,16 @@ async function runCaptureCalibration(input) {
|
|
|
5365
5173
|
});
|
|
5366
5174
|
}
|
|
5367
5175
|
}
|
|
5368
|
-
return {
|
|
5176
|
+
return {
|
|
5177
|
+
calibration,
|
|
5178
|
+
forceScreenshot,
|
|
5179
|
+
probeSession,
|
|
5180
|
+
lastBrowserConsole,
|
|
5181
|
+
calibrationEvaluateTimedOut
|
|
5182
|
+
};
|
|
5183
|
+
}
|
|
5184
|
+
function isEvaluateTimeout(error) {
|
|
5185
|
+
return /Runtime\.evaluate timed out/i.test(normalizeErrorMessage(error));
|
|
5369
5186
|
}
|
|
5370
5187
|
function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
5371
5188
|
const message = normalizeErrorMessage(error);
|
|
@@ -5374,6 +5191,224 @@ function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
|
5374
5191
|
);
|
|
5375
5192
|
}
|
|
5376
5193
|
|
|
5194
|
+
// src/services/render/pageNeverReady.ts
|
|
5195
|
+
var PAGE_NEVER_READY = "PAGE_NEVER_READY";
|
|
5196
|
+
var PageNeverReadyError = class extends Error {
|
|
5197
|
+
code = PAGE_NEVER_READY;
|
|
5198
|
+
// The producer server reads these off the error for the wire envelope.
|
|
5199
|
+
owner = "user";
|
|
5200
|
+
retryable = false;
|
|
5201
|
+
constructor(cause) {
|
|
5202
|
+
super(
|
|
5203
|
+
"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.",
|
|
5204
|
+
{ cause }
|
|
5205
|
+
);
|
|
5206
|
+
this.name = "PageNeverReadyError";
|
|
5207
|
+
}
|
|
5208
|
+
};
|
|
5209
|
+
function checkPageNeverReady(input) {
|
|
5210
|
+
const { calibrationEvaluateTimedOut, framesRendered, error } = input;
|
|
5211
|
+
if (!calibrationEvaluateTimedOut || framesRendered > 0 || !isEvaluateTimeout(error)) {
|
|
5212
|
+
return void 0;
|
|
5213
|
+
}
|
|
5214
|
+
return new PageNeverReadyError(error);
|
|
5215
|
+
}
|
|
5216
|
+
|
|
5217
|
+
// src/services/render/segmentManifest.ts
|
|
5218
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5219
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5220
|
+
import { join as join12 } from "node:path";
|
|
5221
|
+
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
5222
|
+
var MANIFEST_FILENAME = "segments.json";
|
|
5223
|
+
function computeSegmentPlanHash(input) {
|
|
5224
|
+
const ordered = [
|
|
5225
|
+
input.compositionHash,
|
|
5226
|
+
input.cliVersion,
|
|
5227
|
+
String(input.totalFrames),
|
|
5228
|
+
String(input.segmentFrames),
|
|
5229
|
+
`${input.fps.num}/${input.fps.den}`,
|
|
5230
|
+
`${input.width}x${input.height}`,
|
|
5231
|
+
input.codec,
|
|
5232
|
+
input.preset,
|
|
5233
|
+
input.quality === void 0 ? "" : String(input.quality),
|
|
5234
|
+
input.bitrate ?? "",
|
|
5235
|
+
input.pixelFormat ?? "",
|
|
5236
|
+
input.imageFormat,
|
|
5237
|
+
input.useGpu ? "gpu" : "cpu",
|
|
5238
|
+
`${input.outputWidth}x${input.outputHeight}`,
|
|
5239
|
+
input.motionBlur,
|
|
5240
|
+
input.pageMotionBlur ?? "",
|
|
5241
|
+
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
5242
|
+
];
|
|
5243
|
+
const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
|
|
5244
|
+
if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
|
|
5245
|
+
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
5246
|
+
}
|
|
5247
|
+
function segmentDirFor(projectRendersDir, planHash) {
|
|
5248
|
+
return join12(projectRendersDir, ".hf-segments", planHash);
|
|
5249
|
+
}
|
|
5250
|
+
function hasNumber(value, key) {
|
|
5251
|
+
return key in value && typeof Reflect.get(value, key) === "number";
|
|
5252
|
+
}
|
|
5253
|
+
function hasString(value, key) {
|
|
5254
|
+
return key in value && typeof Reflect.get(value, key) === "string";
|
|
5255
|
+
}
|
|
5256
|
+
function isEntry(value) {
|
|
5257
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5258
|
+
return hasNumber(value, "index") && hasNumber(value, "startFrame") && hasNumber(value, "endFrame") && hasString(value, "path") && hasNumber(value, "bytes") && hasString(value, "completedAt");
|
|
5259
|
+
}
|
|
5260
|
+
function isManifest(value) {
|
|
5261
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5262
|
+
const completed = Reflect.get(value, "completed");
|
|
5263
|
+
return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
|
|
5264
|
+
}
|
|
5265
|
+
function readSegmentManifest(segmentDir) {
|
|
5266
|
+
const path = join12(segmentDir, MANIFEST_FILENAME);
|
|
5267
|
+
if (!existsSync7(path)) return null;
|
|
5268
|
+
try {
|
|
5269
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
5270
|
+
return isManifest(parsed) ? parsed : null;
|
|
5271
|
+
} catch {
|
|
5272
|
+
return null;
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
function writeSegmentManifest(segmentDir, manifest) {
|
|
5276
|
+
mkdirSync4(segmentDir, { recursive: true });
|
|
5277
|
+
const tmp = join12(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
|
|
5278
|
+
writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
|
|
5279
|
+
renameWithRetry2(tmp, join12(segmentDir, MANIFEST_FILENAME));
|
|
5280
|
+
}
|
|
5281
|
+
async function probeSegmentFrameCount(path, probe) {
|
|
5282
|
+
try {
|
|
5283
|
+
const meta = await probe(path);
|
|
5284
|
+
if (!Number.isFinite(meta.videoStreamDurationSeconds) || !Number.isFinite(meta.fps))
|
|
5285
|
+
return null;
|
|
5286
|
+
if (meta.fps <= 0) return null;
|
|
5287
|
+
return Math.round(meta.videoStreamDurationSeconds * meta.fps);
|
|
5288
|
+
} catch {
|
|
5289
|
+
return null;
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
|
|
5293
|
+
const ok = /* @__PURE__ */ new Set();
|
|
5294
|
+
if (manifest.planHash !== expectedHash) return ok;
|
|
5295
|
+
for (const entry of manifest.completed) {
|
|
5296
|
+
if (!existsSync7(entry.path)) continue;
|
|
5297
|
+
const size = statSync4(entry.path).size;
|
|
5298
|
+
if (size <= 0 || size !== entry.bytes) continue;
|
|
5299
|
+
const frames = await probeFrames(entry.path);
|
|
5300
|
+
if (frames !== entry.endFrame - entry.startFrame) continue;
|
|
5301
|
+
ok.add(entry.index);
|
|
5302
|
+
}
|
|
5303
|
+
return ok;
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
// src/services/render/hdrMode.ts
|
|
5307
|
+
import {
|
|
5308
|
+
analyzeCompositionHdr,
|
|
5309
|
+
formatHdrAutoPromotionWarning,
|
|
5310
|
+
gradingKeepsSdr,
|
|
5311
|
+
HDR_AUTO_PROMOTION_PIPELINE,
|
|
5312
|
+
isHdrColorSpace,
|
|
5313
|
+
sanitizeHdrAutoPromotionAsset
|
|
5314
|
+
} from "@hyperframes/engine";
|
|
5315
|
+
function formatDowngradeWarning(outputFormat, forcedHdrWithoutSources) {
|
|
5316
|
+
const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
|
|
5317
|
+
const formatReason = outputFormat === "hls" ? "HLS output is SDR-only (H.264 in MPEG-TS)" : "HDR + alpha is not supported";
|
|
5318
|
+
return `[Render] ${hdrSourceReason}, but format is "${outputFormat}", so falling back to SDR. ${formatReason}. Use --format mp4 for HDR10 output.`;
|
|
5319
|
+
}
|
|
5320
|
+
function findRenderHdrAutoPromotionTrigger(input) {
|
|
5321
|
+
for (const extracted of input.extractionResult?.extracted ?? []) {
|
|
5322
|
+
if (!isHdrColorSpace(extracted.metadata.colorSpace)) continue;
|
|
5323
|
+
const source = input.videos.find(({ id }) => id === extracted.videoId)?.src;
|
|
5324
|
+
if (source) return source;
|
|
5325
|
+
}
|
|
5326
|
+
return input.images.find(({ id }) => input.nativeHdrImageIds.has(id))?.src;
|
|
5327
|
+
}
|
|
5328
|
+
function logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource) {
|
|
5329
|
+
const keptSdr = hasHdrSource && gradingKeepsSdr(hdrMode, input.media);
|
|
5330
|
+
if (keptSdr) {
|
|
5331
|
+
input.log.warn(
|
|
5332
|
+
"[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)."
|
|
5333
|
+
);
|
|
5334
|
+
} else if (effectiveHdr && [...input.media.videos, ...input.media.images].some((m) => m.graded)) {
|
|
5335
|
+
input.log.warn(
|
|
5336
|
+
"[Render] HDR output with graded media: color grading is SDR-only, so graded elements are graded in SDR and placed in the HDR output."
|
|
5337
|
+
);
|
|
5338
|
+
}
|
|
5339
|
+
return keptSdr;
|
|
5340
|
+
}
|
|
5341
|
+
function logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, hasHdrSource) {
|
|
5342
|
+
const keptSdrForGrading = logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource);
|
|
5343
|
+
if (forcedHdrWithoutSources) {
|
|
5344
|
+
input.log.warn(
|
|
5345
|
+
"[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."
|
|
5346
|
+
);
|
|
5347
|
+
}
|
|
5348
|
+
if (!effectiveHdr) {
|
|
5349
|
+
if (keptSdrForGrading) return;
|
|
5350
|
+
input.log.info(
|
|
5351
|
+
hdrMode === "force-sdr" ? "[Render] SDR forced by --sdr flag" : "[Render] No HDR sources detected, rendering SDR"
|
|
5352
|
+
);
|
|
5353
|
+
return;
|
|
5354
|
+
}
|
|
5355
|
+
if (hdrMode === "auto") {
|
|
5356
|
+
input.log.warn(
|
|
5357
|
+
formatHdrAutoPromotionWarning({
|
|
5358
|
+
triggeringAsset: sanitizeHdrAutoPromotionAsset(
|
|
5359
|
+
input.autoPromotionTrigger ?? "an HDR-tagged asset"
|
|
5360
|
+
),
|
|
5361
|
+
output: HDR_AUTO_PROMOTION_PIPELINE
|
|
5362
|
+
})
|
|
5363
|
+
);
|
|
5364
|
+
return;
|
|
5365
|
+
}
|
|
5366
|
+
const reason = forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected, defaulting to HLG)" : "forced by --hdr flag";
|
|
5367
|
+
input.log.info(
|
|
5368
|
+
`[Render] HDR ${reason}, output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
5369
|
+
);
|
|
5370
|
+
}
|
|
5371
|
+
function formatCarriesHdr(format) {
|
|
5372
|
+
return format === "mp4";
|
|
5373
|
+
}
|
|
5374
|
+
function rendersSdrOnly(config, media) {
|
|
5375
|
+
return config.hdrMode === "force-sdr" || gradingKeepsSdr(config.hdrMode, media) || !formatCarriesHdr(config.format ?? "mp4");
|
|
5376
|
+
}
|
|
5377
|
+
function resolveEffectiveHdrMode(input) {
|
|
5378
|
+
const hdrMode = input.hdrMode ?? "auto";
|
|
5379
|
+
const videoColorSpaces = (input.extractionResult?.extracted ?? []).map(
|
|
5380
|
+
(ext) => ext.metadata.colorSpace
|
|
5381
|
+
);
|
|
5382
|
+
const allColorSpaces = [...videoColorSpaces, ...input.imageColorSpaces];
|
|
5383
|
+
const info = allColorSpaces.length > 0 ? analyzeCompositionHdr(allColorSpaces) : null;
|
|
5384
|
+
let effectiveHdr;
|
|
5385
|
+
let forcedHdrWithoutSources = false;
|
|
5386
|
+
if (hdrMode === "force-sdr" || gradingKeepsSdr(hdrMode, input.media)) {
|
|
5387
|
+
effectiveHdr = void 0;
|
|
5388
|
+
} else if (hdrMode === "force-hdr") {
|
|
5389
|
+
if (info?.hasHdr && info.dominantTransfer) {
|
|
5390
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5391
|
+
} else {
|
|
5392
|
+
effectiveHdr = { transfer: "hlg" };
|
|
5393
|
+
forcedHdrWithoutSources = true;
|
|
5394
|
+
}
|
|
5395
|
+
} else if (info?.hasHdr && info.dominantTransfer) {
|
|
5396
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
5397
|
+
}
|
|
5398
|
+
if (effectiveHdr && !formatCarriesHdr(input.outputFormat)) {
|
|
5399
|
+
input.log.warn(formatDowngradeWarning(input.outputFormat, forcedHdrWithoutSources));
|
|
5400
|
+
effectiveHdr = void 0;
|
|
5401
|
+
}
|
|
5402
|
+
logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, info?.hasHdr === true);
|
|
5403
|
+
return effectiveHdr;
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
// src/services/render/captureBeyondViewport.ts
|
|
5407
|
+
function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
5408
|
+
if (videoCount <= 0) return void 0;
|
|
5409
|
+
return true;
|
|
5410
|
+
}
|
|
5411
|
+
|
|
5377
5412
|
// src/services/render/observability.ts
|
|
5378
5413
|
import { createHash as createHash3 } from "node:crypto";
|
|
5379
5414
|
import { redactTelemetryString } from "@hyperframes/core";
|
|
@@ -13422,6 +13457,7 @@ async function executeRenderPipeline(input) {
|
|
|
13422
13457
|
const onProgress = execution.onProgress;
|
|
13423
13458
|
const tooSlow = new AbortController();
|
|
13424
13459
|
let tooSlowError;
|
|
13460
|
+
let calibrationEvaluateTimedOut = false;
|
|
13425
13461
|
const executionSignal = job.config.maxRenderMs ? AbortSignal.any([tooSlow.signal, ...execution.signal ? [execution.signal] : []]) : execution.signal;
|
|
13426
13462
|
let fileServer = null;
|
|
13427
13463
|
let probeSession = null;
|
|
@@ -14164,6 +14200,7 @@ async function executeRenderPipeline(input) {
|
|
|
14164
14200
|
{ heartbeatMessage: "browser calibrating (frames not started)" }
|
|
14165
14201
|
);
|
|
14166
14202
|
captureCalibration = outcome.calibration;
|
|
14203
|
+
calibrationEvaluateTimedOut = outcome.calibrationEvaluateTimedOut;
|
|
14167
14204
|
captureForceScreenshot = outcome.forceScreenshot;
|
|
14168
14205
|
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
|
14169
14206
|
probeSession = outcome.probeSession;
|
|
@@ -15314,7 +15351,12 @@ async function executeRenderPipeline(input) {
|
|
|
15314
15351
|
await eventPublisher.flush();
|
|
15315
15352
|
} catch (caught) {
|
|
15316
15353
|
await pendingAudio;
|
|
15317
|
-
const
|
|
15354
|
+
const pageNeverReady = checkPageNeverReady({
|
|
15355
|
+
calibrationEvaluateTimedOut,
|
|
15356
|
+
framesRendered: job.framesRendered ?? 0,
|
|
15357
|
+
error: caught
|
|
15358
|
+
});
|
|
15359
|
+
const error = surfaceScriptFailure(tooSlowError ?? pageNeverReady ?? caught, job);
|
|
15318
15360
|
if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
|
|
15319
15361
|
job.error = error instanceof Error ? error.message : "render_cancelled";
|
|
15320
15362
|
updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
|
|
@@ -16013,7 +16055,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
16013
16055
|
"VIDEO_EXTRACTION_FAILED",
|
|
16014
16056
|
"ENCODER_INTERRUPTED",
|
|
16015
16057
|
"RENDER_TOO_SLOW",
|
|
16016
|
-
"RENDER_STAGE_STALLED"
|
|
16058
|
+
"RENDER_STAGE_STALLED",
|
|
16059
|
+
"PAGE_NEVER_READY"
|
|
16017
16060
|
]);
|
|
16018
16061
|
function extractSafeRenderErrorCode(error) {
|
|
16019
16062
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.12",
|
|
4
4
|
"description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"postcss": "^8.4.0",
|
|
36
36
|
"puppeteer": "~25.11.0",
|
|
37
37
|
"puppeteer-core": "^25.11.0",
|
|
38
|
-
"@hyperframes/
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/
|
|
38
|
+
"@hyperframes/core": "0.9.12",
|
|
39
|
+
"@hyperframes/engine": "0.9.12",
|
|
40
|
+
"@hyperframes/lint": "0.9.12",
|
|
41
|
+
"@hyperframes/parsers": "0.9.12",
|
|
42
|
+
"@hyperframes/studio-server": "0.9.12"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^25.0.10",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"tsx": "^4.21.0",
|
|
49
49
|
"typescript": "^5.7.2",
|
|
50
50
|
"vitest": "^4.1.11",
|
|
51
|
-
"@hyperframes/aws-lambda": "0.9.
|
|
51
|
+
"@hyperframes/aws-lambda": "0.9.12"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|