@hyperframes/producer 0.9.12 → 0.9.14

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;
@@ -3756,6 +3754,7 @@ function createStallClock(now = () => Date.now()) {
3756
3754
 
3757
3755
  // src/services/render/stages/captureStreamingStage.ts
3758
3756
  var DEFAULT_CAPTURE_STALL_MS = 6e4;
3757
+ var SESSION_INIT_STALL_MS = 3e5;
3759
3758
  var DE_STALL_POLL_MS = 5e3;
3760
3759
  var STALL_ADVICE = "Chrome stopped answering. If another app is keeping the GPU or CPU busy (a 3D render, a game, another export), let it finish or render one job at a time, then retry.";
3761
3760
  function poolFailure(error, probeSession) {
@@ -3786,6 +3785,14 @@ function captureModeLabel(mode) {
3786
3785
  if (mode === "beginframe") return "BeginFrame";
3787
3786
  return "screenshot";
3788
3787
  }
3788
+ var OPENING_CAPTURE_SESSION = {
3789
+ captureMode: "screenshot",
3790
+ captureStep: "opening the capture session"
3791
+ };
3792
+ var OPENING_RISK_SESSION = {
3793
+ captureMode: "screenshot",
3794
+ captureStep: "opening the screenshot session for effect frames"
3795
+ };
3789
3796
  var SequentialCaptureStallError = class extends Error {
3790
3797
  captureMode;
3791
3798
  constructor(session, stallTimeoutMs, frameIndex, totalFrames) {
@@ -3935,7 +3942,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3935
3942
  const guard = createDrainFrameGuard({ log, stats, frameTime });
3936
3943
  const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
3937
3944
  const captureRisk = async (idx) => {
3938
- await risk.open();
3945
+ await raceAgainstStall(
3946
+ risk.open(),
3947
+ stallTimeoutMs - quiet.idle(0),
3948
+ { session: OPENING_RISK_SESSION, frameIndex: idx, totalFrames, stallTimeoutMs },
3949
+ abortSignal
3950
+ );
3939
3951
  quiet.reset();
3940
3952
  return raceAgainstStall(
3941
3953
  risk.capture(idx, frameTime(idx)),
@@ -4294,19 +4306,38 @@ async function runCaptureStreamingStage(input) {
4294
4306
  let resumes = 0;
4295
4307
  for (; ; ) {
4296
4308
  const videoInjector = createRenderVideoFrameInjector();
4297
- const session = probeSession ?? await createCaptureSession(
4298
- fileServer.url,
4299
- framesDir,
4300
- buildCaptureOptions(),
4301
- videoInjector,
4302
- captureCfg
4309
+ const initStallMs = Math.max(SESSION_INIT_STALL_MS, resolveCaptureStallTimeoutMs());
4310
+ const setup = createStallClock();
4311
+ const withinInitBudget = (work, stalled) => raceAgainstStall(
4312
+ work,
4313
+ initStallMs - setup.idle(0),
4314
+ { session: stalled, frameIndex: resume.from, totalFrames, stallTimeoutMs: initStallMs },
4315
+ abortSignal
4303
4316
  );
4317
+ const openSession = async () => {
4318
+ const opening = createCaptureSession(
4319
+ fileServer.url,
4320
+ framesDir,
4321
+ buildCaptureOptions(),
4322
+ videoInjector,
4323
+ captureCfg
4324
+ );
4325
+ try {
4326
+ return await withinInitBudget(opening, OPENING_CAPTURE_SESSION);
4327
+ } catch (error) {
4328
+ void opening.then(closeCaptureSession, () => {
4329
+ });
4330
+ throw error;
4331
+ }
4332
+ };
4333
+ const session = probeSession ?? await openSession();
4304
4334
  captureBeyondViewport = session.options.captureBeyondViewport;
4305
4335
  if (probeSession) {
4306
4336
  prepareCaptureSessionForReuse(session, framesDir, videoInjector);
4307
4337
  probeSession = null;
4308
4338
  }
4309
4339
  let riskSession = null;
4340
+ let attemptClosed = false;
4310
4341
  const openRiskSession = async () => {
4311
4342
  const opened = await createCaptureSession(
4312
4343
  fileServer.url,
@@ -4320,6 +4351,10 @@ async function runCaptureStreamingStage(input) {
4320
4351
  enableBrowserPool: false
4321
4352
  }
4322
4353
  );
4354
+ if (attemptClosed) {
4355
+ await closeCaptureSession(opened);
4356
+ throw new Error("[Render] Capture attempt ended while its screenshot session opened");
4357
+ }
4323
4358
  riskSession = opened;
4324
4359
  await initializeSession(opened);
4325
4360
  return opened;
@@ -4327,9 +4362,9 @@ async function runCaptureStreamingStage(input) {
4327
4362
  let resumed = false;
4328
4363
  try {
4329
4364
  if (!session.isInitialized) {
4330
- await initializeSession(session);
4365
+ await withinInitBudget(initializeSession(session), session);
4331
4366
  }
4332
- await completeDeferredDrawElementInit(session);
4367
+ await withinInitBudget(completeDeferredDrawElementInit(session), session);
4333
4368
  assertNotAborted();
4334
4369
  lastBrowserConsole = session.browserConsoleBuffer;
4335
4370
  if (session.workerEncodeEnabled) {
@@ -4412,6 +4447,7 @@ async function runCaptureStreamingStage(input) {
4412
4447
  resumes++;
4413
4448
  resumed = true;
4414
4449
  } finally {
4450
+ attemptClosed = true;
4415
4451
  lastBrowserConsole = session.browserConsoleBuffer;
4416
4452
  await closeCaptureSession(session);
4417
4453
  if (riskSession) await closeCaptureSession(riskSession);
@@ -5878,7 +5914,7 @@ import { suggestMatchingPreset } from "@hyperframes/core";
5878
5914
  // src/services/htmlCompiler.ts
5879
5915
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5880
5916
  import { createHash as createHash6 } from "node:crypto";
5881
- import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
5917
+ import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
5882
5918
  import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
5883
5919
  import { parseHTML as parseHTML5 } from "linkedom";
5884
5920
  import {
@@ -7853,6 +7889,25 @@ ${fontFaceBlocks.join("\n")}
7853
7889
  }
7854
7890
  return result;
7855
7891
  }
7892
+ function projectStylesheetFontFamilies(html, projectDir) {
7893
+ const families = [];
7894
+ for (const [tag] of html.matchAll(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi)) {
7895
+ const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1];
7896
+ if (!href || /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href)) continue;
7897
+ const sheet = resolve7(projectDir, decodedUrlPath(href).replace(/^(?:\.?\/)+/, ""));
7898
+ if (!isPathInside2(sheet, projectDir) || !existsSync10(sheet)) continue;
7899
+ for (const face of extractFontFaceBlocks(readFileSync7(sheet, "utf8"))) {
7900
+ const family = /font-family\s*:\s*([^;}]+)/i.exec(face)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
7901
+ const shipsFile = [...face.matchAll(LOCAL_FONTFACE_URL_RE)].some(([, url = ""]) => {
7902
+ const path = decodedUrlPath(url);
7903
+ const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname7(sheet), path);
7904
+ return isPathInside2(file, projectDir) && existsSync10(file);
7905
+ });
7906
+ if (family && shipsFile) families.push(family);
7907
+ }
7908
+ }
7909
+ return families;
7910
+ }
7856
7911
  function collectFontFaceUrls(html) {
7857
7912
  const styleBlockRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
7858
7913
  const urlSet = /* @__PURE__ */ new Set();
@@ -7883,12 +7938,10 @@ function collectFontFaceUrls(html) {
7883
7938
  }
7884
7939
  return urlSet;
7885
7940
  }
7886
- async function localizeRemoteFontFaces(html, downloadDir) {
7887
- const processed = await inlineExternalFontStylesheets(html);
7888
- const urlSet = collectFontFaceUrls(processed);
7941
+ function localizeFontFaceUrls(html, downloadDir) {
7889
7942
  return downloadAndRewriteUrls(
7890
- urlSet,
7891
- processed,
7943
+ collectFontFaceUrls(html),
7944
+ html,
7892
7945
  join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7893
7946
  "Remote font download failed",
7894
7947
  "Localized remote font face(s)",
@@ -8056,15 +8109,17 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
8056
8109
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
8057
8110
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
8058
8111
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
8112
+ const fontSheetHtml = await inlineExternalFontStylesheets(
8113
+ promoteCssImportsToLinkTags(sanitizedHtml)
8114
+ );
8059
8115
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
8060
- injectTextRenderingRule(
8061
- coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
8062
- )
8116
+ injectTextRenderingRule(coalesceHeadStylesAndBodyScripts(fontSheetHtml))
8063
8117
  );
8064
8118
  const coalescedHtml = await injectDeterministicFontFaces(normalizedFontHtml, {
8065
8119
  logger: defaultLogger,
8066
8120
  failClosedFontFetch: options.failClosedFontFetch === true,
8067
8121
  allowSystemFontCapture: options.allowSystemFontCapture,
8122
+ projectFontFamilies: projectStylesheetFontFamilies(fontSheetHtml, projectDir),
8068
8123
  abortSignal: options.abortSignal
8069
8124
  });
8070
8125
  const assembledHtml = await inlineExternalScripts(coalescedHtml);
@@ -8090,7 +8145,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
8090
8145
  );
8091
8146
  const { html: htmlWithLocalImages, remoteMediaAssets: remoteImageAssets } = await localizeRemoteImageSources(htmlWithLocalMedia, downloadDir);
8092
8147
  const { html: htmlWithLocalBg, remoteMediaAssets: remoteBgAssets } = await localizeRemoteBackgroundImages(htmlWithLocalImages, downloadDir);
8093
- const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeRemoteFontFaces(htmlWithLocalBg, downloadDir);
8148
+ const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeFontFaceUrls(htmlWithLocalBg, downloadDir);
8094
8149
  const {
8095
8150
  html: htmlWithJpegs,
8096
8151
  preparedAssets: preparedHeifAssets,
@@ -9508,13 +9563,20 @@ var VideoExtractionStageError = class extends Error {
9508
9563
  */
9509
9564
  publicMetadata;
9510
9565
  };
9511
- function throwIfEncoderInterrupted(result) {
9566
+ function throwIfHostFailure(result) {
9512
9567
  const interrupted = result.errors.find((failure) => failure.kind === "external_interruption");
9513
- if (!interrupted) return;
9514
- throw encoderFailureError("Video frame extraction failed", {
9515
- error: String(interrupted.error),
9516
- failureReason: "external_interruption"
9517
- });
9568
+ if (interrupted) {
9569
+ throw encoderFailureError("Video frame extraction failed", {
9570
+ error: String(interrupted.error),
9571
+ failureReason: "external_interruption"
9572
+ });
9573
+ }
9574
+ const full = result.errors.filter((failure) => failure.kind === "disk_full").length;
9575
+ if (full === 0) return;
9576
+ throw Object.assign(
9577
+ new Error(`ENOSPC: no space left on device, extracting the frames of ${full} video source(s)`),
9578
+ { code: "ENOSPC" }
9579
+ );
9518
9580
  }
9519
9581
  function buildVideoExtractionStageError(result) {
9520
9582
  if (result.success && result.errors.length === 0) return null;
@@ -9729,7 +9791,7 @@ async function runExtractVideosStage(input) {
9729
9791
  );
9730
9792
  extractionResult.phaseBreakdown.transientRetries = (extractionResult.phaseBreakdown.transientRetries ?? 0) + hdrProbeTransientRetries;
9731
9793
  assertNotAborted();
9732
- throwIfEncoderInterrupted(extractionResult);
9794
+ throwIfHostFailure(extractionResult);
9733
9795
  failureToEnforce = applyVideoExtractionFailurePolicy(extractionResult, extractionPolicy, log);
9734
9796
  materializeExtractedFramesForCompiledDir(extractionResult.extracted, compiledDir, {
9735
9797
  materializeSymlinks
@@ -11921,7 +11983,7 @@ import {
11921
11983
  existsSync as existsSync17,
11922
11984
  mkdirSync as mkdirSync9,
11923
11985
  readdirSync as readdirSync4,
11924
- readFileSync as readFileSync7,
11986
+ readFileSync as readFileSync8,
11925
11987
  rmSync as rmSync9,
11926
11988
  statSync as statSync7,
11927
11989
  writeFileSync as writeFileSync5
@@ -12015,7 +12077,7 @@ async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
12015
12077
  const error = formatFfmpegError(result.exitCode, result.stderr);
12016
12078
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
12017
12079
  }
12018
- const gif = readFileSync7(outputPath);
12080
+ const gif = readFileSync8(outputPath);
12019
12081
  if (!clearGifFramesBeforeNext(gif)) {
12020
12082
  const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
12021
12083
  return gifEncodeFailure(startTime, outputPath, error);
@@ -12048,7 +12110,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12048
12110
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
12049
12111
  }
12050
12112
  }
12051
- if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync7(outputPath)) !== false) {
12113
+ if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync8(outputPath)) !== false) {
12052
12114
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
12053
12115
  if (failure) return failure;
12054
12116
  }
@@ -12867,6 +12929,22 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
12867
12929
  }
12868
12930
  const remainingCount = countFrameRanges(remaining);
12869
12931
  const madeProgress = captureAttemptMadeProgress(frameCount, remainingCount);
12932
+ 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)) {
12933
+ initializationRetriesUsed++;
12934
+ const nextWorkers2 = getNextRetryWorkerCount(currentWorkers);
12935
+ options.log.warn(
12936
+ "[Render] Browser initialization timed out; retrying with fewer workers.",
12937
+ {
12938
+ fromWorkers: currentWorkers,
12939
+ toWorkers: nextWorkers2,
12940
+ error: failure.message
12941
+ }
12942
+ );
12943
+ currentWorkers = nextWorkers2;
12944
+ missingRanges = remaining;
12945
+ attempt++;
12946
+ continue;
12947
+ }
12870
12948
  if (options.allowRetry && isTransientCaptureRetryEligible(failure.kind, remaining, transientRetriesUsed)) {
12871
12949
  transientRetriesUsed++;
12872
12950
  options.log.warn(
@@ -12884,22 +12962,6 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
12884
12962
  pendingTransientRetry = true;
12885
12963
  continue;
12886
12964
  }
12887
- if (options.allowRetry && !madeProgress && initializationRetriesUsed === 0 && failure.kind === "protocol_timeout" && /\bNetwork\.enable timed out/i.test(failure.message)) {
12888
- initializationRetriesUsed++;
12889
- const nextWorkers2 = getNextRetryWorkerCount(currentWorkers);
12890
- options.log.warn(
12891
- "[Render] Browser initialization timed out; retrying once with fresh sessions.",
12892
- {
12893
- fromWorkers: currentWorkers,
12894
- toWorkers: nextWorkers2,
12895
- error: failure.message
12896
- }
12897
- );
12898
- currentWorkers = nextWorkers2;
12899
- missingRanges = remaining;
12900
- attempt++;
12901
- continue;
12902
- }
12903
12965
  if (!madeProgress) {
12904
12966
  options.log.warn(
12905
12967
  "[Render] Capture attempt made no forward progress; composition is likely structurally broken, not retrying.",
@@ -15450,7 +15512,7 @@ async function executeRenderPipeline(input) {
15450
15512
  // src/services/hyperframeLint.ts
15451
15513
  import {
15452
15514
  existsSync as existsSync19,
15453
- readFileSync as readFileSync8,
15515
+ readFileSync as readFileSync9,
15454
15516
  statSync as statSync9,
15455
15517
  openSync as openSync5,
15456
15518
  fstatSync as fstatSync3,
@@ -15496,7 +15558,7 @@ function readEntryHtml(filePath) {
15496
15558
  }
15497
15559
  try {
15498
15560
  if (!fstatSync3(fd).isFile()) return null;
15499
- return readFileSync8(fd, "utf-8");
15561
+ return readFileSync9(fd, "utf-8");
15500
15562
  } finally {
15501
15563
  closeSync5(fd);
15502
15564
  }
@@ -44,7 +44,10 @@ import type { ProducerLogger } from "../../../logger.js";
44
44
  import type { ProgressCallback, RenderJob } from "../../renderOrchestrator.js";
45
45
  import type { SdrStreamingCapturePlan } from "../capturePlan.js";
46
46
  export declare function resolveCaptureStallTimeoutMs(): number;
47
- type StalledSession = Pick<CaptureSession, "captureMode" | "captureStep">;
47
+ type StalledSession = {
48
+ captureMode: CaptureSession["captureMode"];
49
+ captureStep?: string;
50
+ };
48
51
  export declare function raceAgainstStall<T>(promise: Promise<T>, deadlineMs: number, input: {
49
52
  session: StalledSession;
50
53
  frameIndex: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
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/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"
38
+ "@hyperframes/core": "0.9.14",
39
+ "@hyperframes/lint": "0.9.14",
40
+ "@hyperframes/engine": "0.9.14",
41
+ "@hyperframes/studio-server": "0.9.14",
42
+ "@hyperframes/parsers": "0.9.14"
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.12"
51
+ "@hyperframes/aws-lambda": "0.9.14"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"