@hyperframes/producer 0.9.11 → 0.9.13

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