@hyperframes/producer 0.9.11 → 0.9.13

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