@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.
@@ -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 join12 } from "node:path";
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(join12(workDir, "capture-calibration"), calibrationCfg);
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
- join12(workDir, "capture-calibration-screenshot"),
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 { calibration, forceScreenshot, probeSession, lastBrowserConsole };
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 error = surfaceScriptFailure(tooSlowError ?? caught, job);
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;
@@ -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 join12 } from "node:path";
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(join12(workDir, "capture-calibration"), calibrationCfg);
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
- join12(workDir, "capture-calibration-screenshot"),
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 { calibration, forceScreenshot, probeSession, lastBrowserConsole };
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 error = surfaceScriptFailure(tooSlowError ?? caught, job);
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
@@ -0,0 +1,6 @@
1
+ /** Calibration and capture both timed out on `Runtime.evaluate` with no frame captured. */
2
+ export declare function checkPageNeverReady(input: {
3
+ calibrationEvaluateTimedOut: boolean;
4
+ framesRendered: number;
5
+ error: unknown;
6
+ }): Error | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.9.10",
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/engine": "0.9.10",
39
- "@hyperframes/lint": "0.9.10",
40
- "@hyperframes/parsers": "0.9.10",
41
- "@hyperframes/studio-server": "0.9.10",
42
- "@hyperframes/core": "0.9.10"
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.10"
51
+ "@hyperframes/aws-lambda": "0.9.12"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"