@hyperframes/producer 0.9.13 → 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.
@@ -3754,6 +3754,7 @@ function createStallClock(now = () => Date.now()) {
3754
3754
 
3755
3755
  // src/services/render/stages/captureStreamingStage.ts
3756
3756
  var DEFAULT_CAPTURE_STALL_MS = 6e4;
3757
+ var SESSION_INIT_STALL_MS = 3e5;
3757
3758
  var DE_STALL_POLL_MS = 5e3;
3758
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.";
3759
3760
  function poolFailure(error, probeSession) {
@@ -3784,6 +3785,14 @@ function captureModeLabel(mode) {
3784
3785
  if (mode === "beginframe") return "BeginFrame";
3785
3786
  return "screenshot";
3786
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
+ };
3787
3796
  var SequentialCaptureStallError = class extends Error {
3788
3797
  captureMode;
3789
3798
  constructor(session, stallTimeoutMs, frameIndex, totalFrames) {
@@ -3933,7 +3942,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3933
3942
  const guard = createDrainFrameGuard({ log, stats, frameTime });
3934
3943
  const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
3935
3944
  const captureRisk = async (idx) => {
3936
- 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
+ );
3937
3951
  quiet.reset();
3938
3952
  return raceAgainstStall(
3939
3953
  risk.capture(idx, frameTime(idx)),
@@ -4292,19 +4306,38 @@ async function runCaptureStreamingStage(input) {
4292
4306
  let resumes = 0;
4293
4307
  for (; ; ) {
4294
4308
  const videoInjector = createRenderVideoFrameInjector();
4295
- const session = probeSession ?? await createCaptureSession(
4296
- fileServer.url,
4297
- framesDir,
4298
- buildCaptureOptions(),
4299
- videoInjector,
4300
- 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
4301
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();
4302
4334
  captureBeyondViewport = session.options.captureBeyondViewport;
4303
4335
  if (probeSession) {
4304
4336
  prepareCaptureSessionForReuse(session, framesDir, videoInjector);
4305
4337
  probeSession = null;
4306
4338
  }
4307
4339
  let riskSession = null;
4340
+ let attemptClosed = false;
4308
4341
  const openRiskSession = async () => {
4309
4342
  const opened = await createCaptureSession(
4310
4343
  fileServer.url,
@@ -4318,6 +4351,10 @@ async function runCaptureStreamingStage(input) {
4318
4351
  enableBrowserPool: false
4319
4352
  }
4320
4353
  );
4354
+ if (attemptClosed) {
4355
+ await closeCaptureSession(opened);
4356
+ throw new Error("[Render] Capture attempt ended while its screenshot session opened");
4357
+ }
4321
4358
  riskSession = opened;
4322
4359
  await initializeSession(opened);
4323
4360
  return opened;
@@ -4325,9 +4362,9 @@ async function runCaptureStreamingStage(input) {
4325
4362
  let resumed = false;
4326
4363
  try {
4327
4364
  if (!session.isInitialized) {
4328
- await initializeSession(session);
4365
+ await withinInitBudget(initializeSession(session), session);
4329
4366
  }
4330
- await completeDeferredDrawElementInit(session);
4367
+ await withinInitBudget(completeDeferredDrawElementInit(session), session);
4331
4368
  assertNotAborted();
4332
4369
  lastBrowserConsole = session.browserConsoleBuffer;
4333
4370
  if (session.workerEncodeEnabled) {
@@ -4410,6 +4447,7 @@ async function runCaptureStreamingStage(input) {
4410
4447
  resumes++;
4411
4448
  resumed = true;
4412
4449
  } finally {
4450
+ attemptClosed = true;
4413
4451
  lastBrowserConsole = session.browserConsoleBuffer;
4414
4452
  await closeCaptureSession(session);
4415
4453
  if (riskSession) await closeCaptureSession(riskSession);
@@ -5876,7 +5914,7 @@ import { suggestMatchingPreset } from "@hyperframes/core";
5876
5914
  // src/services/htmlCompiler.ts
5877
5915
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5878
5916
  import { createHash as createHash6 } from "node:crypto";
5879
- 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";
5880
5918
  import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
5881
5919
  import { parseHTML as parseHTML5 } from "linkedom";
5882
5920
  import {
@@ -7851,6 +7889,25 @@ ${fontFaceBlocks.join("\n")}
7851
7889
  }
7852
7890
  return result;
7853
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
+ }
7854
7911
  function collectFontFaceUrls(html) {
7855
7912
  const styleBlockRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
7856
7913
  const urlSet = /* @__PURE__ */ new Set();
@@ -7881,12 +7938,10 @@ function collectFontFaceUrls(html) {
7881
7938
  }
7882
7939
  return urlSet;
7883
7940
  }
7884
- async function localizeRemoteFontFaces(html, downloadDir) {
7885
- const processed = await inlineExternalFontStylesheets(html);
7886
- const urlSet = collectFontFaceUrls(processed);
7941
+ function localizeFontFaceUrls(html, downloadDir) {
7887
7942
  return downloadAndRewriteUrls(
7888
- urlSet,
7889
- processed,
7943
+ collectFontFaceUrls(html),
7944
+ html,
7890
7945
  join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7891
7946
  "Remote font download failed",
7892
7947
  "Localized remote font face(s)",
@@ -8054,15 +8109,17 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
8054
8109
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
8055
8110
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
8056
8111
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
8112
+ const fontSheetHtml = await inlineExternalFontStylesheets(
8113
+ promoteCssImportsToLinkTags(sanitizedHtml)
8114
+ );
8057
8115
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
8058
- injectTextRenderingRule(
8059
- coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
8060
- )
8116
+ injectTextRenderingRule(coalesceHeadStylesAndBodyScripts(fontSheetHtml))
8061
8117
  );
8062
8118
  const coalescedHtml = await injectDeterministicFontFaces(normalizedFontHtml, {
8063
8119
  logger: defaultLogger,
8064
8120
  failClosedFontFetch: options.failClosedFontFetch === true,
8065
8121
  allowSystemFontCapture: options.allowSystemFontCapture,
8122
+ projectFontFamilies: projectStylesheetFontFamilies(fontSheetHtml, projectDir),
8066
8123
  abortSignal: options.abortSignal
8067
8124
  });
8068
8125
  const assembledHtml = await inlineExternalScripts(coalescedHtml);
@@ -8088,7 +8145,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
8088
8145
  );
8089
8146
  const { html: htmlWithLocalImages, remoteMediaAssets: remoteImageAssets } = await localizeRemoteImageSources(htmlWithLocalMedia, downloadDir);
8090
8147
  const { html: htmlWithLocalBg, remoteMediaAssets: remoteBgAssets } = await localizeRemoteBackgroundImages(htmlWithLocalImages, downloadDir);
8091
- const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeRemoteFontFaces(htmlWithLocalBg, downloadDir);
8148
+ const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeFontFaceUrls(htmlWithLocalBg, downloadDir);
8092
8149
  const {
8093
8150
  html: htmlWithJpegs,
8094
8151
  preparedAssets: preparedHeifAssets,
@@ -11926,7 +11983,7 @@ import {
11926
11983
  existsSync as existsSync17,
11927
11984
  mkdirSync as mkdirSync9,
11928
11985
  readdirSync as readdirSync4,
11929
- readFileSync as readFileSync7,
11986
+ readFileSync as readFileSync8,
11930
11987
  rmSync as rmSync9,
11931
11988
  statSync as statSync7,
11932
11989
  writeFileSync as writeFileSync5
@@ -12020,7 +12077,7 @@ async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
12020
12077
  const error = formatFfmpegError(result.exitCode, result.stderr);
12021
12078
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
12022
12079
  }
12023
- const gif = readFileSync7(outputPath);
12080
+ const gif = readFileSync8(outputPath);
12024
12081
  if (!clearGifFramesBeforeNext(gif)) {
12025
12082
  const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
12026
12083
  return gifEncodeFailure(startTime, outputPath, error);
@@ -12053,7 +12110,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12053
12110
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
12054
12111
  }
12055
12112
  }
12056
- if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync7(outputPath)) !== false) {
12113
+ if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync8(outputPath)) !== false) {
12057
12114
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
12058
12115
  if (failure) return failure;
12059
12116
  }
@@ -15455,7 +15512,7 @@ async function executeRenderPipeline(input) {
15455
15512
  // src/services/hyperframeLint.ts
15456
15513
  import {
15457
15514
  existsSync as existsSync19,
15458
- readFileSync as readFileSync8,
15515
+ readFileSync as readFileSync9,
15459
15516
  statSync as statSync9,
15460
15517
  openSync as openSync5,
15461
15518
  fstatSync as fstatSync3,
@@ -15501,7 +15558,7 @@ function readEntryHtml(filePath) {
15501
15558
  }
15502
15559
  try {
15503
15560
  if (!fstatSync3(fd).isFile()) return null;
15504
- return readFileSync8(fd, "utf-8");
15561
+ return readFileSync9(fd, "utf-8");
15505
15562
  } finally {
15506
15563
  closeSync5(fd);
15507
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.13",
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/lint": "0.9.13",
39
- "@hyperframes/core": "0.9.13",
40
- "@hyperframes/engine": "0.9.13",
41
- "@hyperframes/parsers": "0.9.13",
42
- "@hyperframes/studio-server": "0.9.13"
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.13"
51
+ "@hyperframes/aws-lambda": "0.9.14"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"