@hyperframes/engine 0.8.167 → 0.8.169

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/config.d.ts CHANGED
@@ -5,13 +5,14 @@
5
5
  * env var sprawl with a structured interface. Env vars still work as
6
6
  * fallbacks for backward compatibility during migration.
7
7
  */
8
+ import { type DistributedFps } from "@hyperframes/core/distributed-fps";
8
9
  /**
9
10
  * Full engine configuration. All fields are wired through the config
10
11
  * object; env vars serve as backward-compatible fallbacks resolved
11
12
  * in `resolveConfig()`.
12
13
  */
13
14
  export interface EngineConfig {
14
- fps: 24 | 30 | 60;
15
+ fps: DistributedFps;
15
16
  quality: "draft" | "standard" | "high";
16
17
  format: "jpeg" | "png";
17
18
  jpegQuality: number;
package/dist/config.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * fallbacks for backward compatibility during migration.
7
7
  */
8
8
  import { tmpdir } from "node:os";
9
+ import { DISTRIBUTED_FPS } from "@hyperframes/core/distributed-fps";
9
10
  import { chromeMajorCeiling } from "./services/chromeHostCeiling.js";
10
11
  import { join } from "node:path";
11
12
  import { getSystemTotalMb, isLowMemorySystem, LOW_MEMORY_TOTAL_MB_THRESHOLD, } from "./services/systemMemory.js";
@@ -96,7 +97,7 @@ const POSITIVE_NUMBER_ENGINE_CONFIG_FIELDS = [
96
97
  "extractCacheMaxBytes",
97
98
  ];
98
99
  const ENUM_ENGINE_CONFIG_FIELDS = {
99
- fps: [24, 30, 60],
100
+ fps: DISTRIBUTED_FPS,
100
101
  quality: ["draft", "standard", "high"],
101
102
  format: ["jpeg", "png"],
102
103
  browserGpuMode: ["software", "hardware", "auto"],
@@ -122,7 +122,7 @@ type GpuConfig = Partial<Pick<EngineConfig, "browserGpuMode" | "disableGpu" | "c
122
122
  */
123
123
  export declare function usesSoftwareWebGpu(requiresWebGpu: boolean | undefined, config?: GpuConfig): boolean;
124
124
  export declare function buildChromeArgs(options: BuildChromeArgsOptions, config?: GpuConfig): string[];
125
- /** Does the composition's root element declare `data-requires-webgpu`? */
125
+ /** Does the root, or a mounted root (data-hf-inner-root once bundled), declare `data-requires-webgpu`? */
126
126
  export declare function compositionRequiresWebGpu(html: string): boolean;
127
127
  /** No hardware WebGPU adapter on this host; distinct from a browser or navigation failure. */
128
128
  export declare class WebGpuUnavailableError extends Error {
@@ -879,12 +879,13 @@ export function buildChromeArgs(options, config) {
879
879
  }
880
880
  return chromeArgs;
881
881
  }
882
- /** Does the composition's root element declare `data-requires-webgpu`? */
882
+ /** Does the root, or a mounted root (data-hf-inner-root once bundled), declare `data-requires-webgpu`? */
883
883
  export function compositionRequiresWebGpu(html) {
884
884
  for (const tag of scanHtmlOpeningTags(html)) {
885
885
  const names = tag.attributes.map((attribute) => attribute.name);
886
- if (names.includes("data-composition-id"))
887
- return names.includes("data-requires-webgpu");
886
+ const isRoot = names.includes("data-composition-id") || names.includes("data-hf-inner-root");
887
+ if (isRoot && names.includes("data-requires-webgpu"))
888
+ return true;
888
889
  }
889
890
  return false;
890
891
  }
@@ -43,6 +43,8 @@ export interface MuxVideoWithAudioOptions extends Partial<Pick<EngineConfig, "ff
43
43
  * depend on the file extension alone.
44
44
  */
45
45
  audioCodec?: "aac";
46
+ /** Replaces `-i <videoPath>` (which is then unused), e.g. with a concat-demuxer input. */
47
+ videoInputArgs?: readonly string[];
46
48
  /**
47
49
  * @deprecated No longer used. `-avoid_negative_ts` is never passed for
48
50
  * mp4/mov muxing (ffmpeg's `auto` default already resolves to `disabled`
@@ -587,7 +587,7 @@ export async function muxVideoWithAudio(videoPath, audioPath, outputPath, signal
587
587
  const isWebm = outputPath.endsWith(".webm");
588
588
  const isMov = outputPath.endsWith(".mov");
589
589
  const shouldCopyAudio = isWebm ? false : await shouldCopyAacSidecar(audioPath, config);
590
- const args = ["-i", videoPath, "-i", audioPath, "-c:v", "copy"];
590
+ const args = [...(config?.videoInputArgs ?? ["-i", videoPath]), "-i", audioPath, "-c:v", "copy"];
591
591
  if (isWebm) {
592
592
  args.push("-c:a", "libopus", "-b:a", "128k");
593
593
  }
@@ -12,7 +12,7 @@ import { type MotionBlurPlan } from "./motionBlur.js";
12
12
  import { type BrowserLease, type CaptureMode } from "./browserManager.js";
13
13
  import { pageScreenshotCapture } from "./screenshotService.js";
14
14
  import { type EngineConfig } from "../config.js";
15
- import type { CaptureOptions, CaptureResult, CaptureBufferResult, CapturePerfSummary, CaptureWarning, SubTimelineWaitMemo, SubTimelineWaitOutcome } from "../types.js";
15
+ import type { CaptureOptions, CaptureVideoMetadataHint, CaptureResult, CaptureBufferResult, CapturePerfSummary, CaptureWarning, SubTimelineWaitMemo, SubTimelineWaitOutcome } from "../types.js";
16
16
  import { type ChromeMemorySampler, type ChromePids } from "./chromeMemorySampler.js";
17
17
  export { isMemoryExhaustionError, isTransientBrowserError } from "./captureFailure.js";
18
18
  export type { CaptureOptions, CaptureResult, CaptureBufferResult, CapturePerfSummary };
@@ -427,6 +427,7 @@ export declare function recordSubTimelineWarning(session: CaptureSession, timeou
427
427
  * PRINFRA-300 (blank hook scene, zero diagnostics, "fixed" by re-render).
428
428
  */
429
429
  export declare function buildLiveMapWarning(libraries: readonly string[]): CaptureWarning;
430
+ export declare function applyVideoMetadataHints(page: Page, hints: readonly CaptureVideoMetadataHint[] | undefined): Promise<void>;
430
431
  /** Thrown once a VFX chain fails: the frames it paints would render without the effect. */
431
432
  export declare class VfxFailureError extends Error {
432
433
  readonly detail: string;
@@ -434,14 +434,14 @@ async function waitForCloseWithTimeout(promise) {
434
434
  * screenshots carry a real alpha channel (Chrome resets the override on every
435
435
  * navigation, so this must run after page load).
436
436
  *
437
- * drawElement is also skipped when supersampling (deviceScaleFactor > 1):
437
+ * drawElement is also skipped when scaling (deviceScaleFactor other than 1):
438
438
  * `drawElementImage` reads the canvas at CSS pixels and has no equivalent of
439
439
  * `Page.captureScreenshot`'s clip+scale, so it would silently capture at 1x and
440
440
  * drop the requested supersample. Such renders fall through to the screenshot
441
- * path (preMode already forces "screenshot" for DPR > 1).
441
+ * path (preMode already forces "screenshot" for DPR other than 1).
442
442
  */
443
443
  async function initDrawElementOrTransparentBackground(session, page, logInitPhase) {
444
- const supersampling = (session.options.deviceScaleFactor ?? 1) > 1;
444
+ const supersampling = (session.options.deviceScaleFactor ?? 1) !== 1;
445
445
  // forceScreenshot is an explicit routing decision made upstream (render-mode
446
446
  // compat hints like raw requestAnimationFrame, alpha formats, low-memory) —
447
447
  // drawElement must not override it. Concretely: an rAF-compat comp on
@@ -469,7 +469,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
469
469
  session.deGateReason = "supersampling";
470
470
  session.deFallbackTrigger = "supersampling";
471
471
  console.log("[engine] --experimental-fast-capture disabled for this render: drawElementImage " +
472
- "ignores deviceScaleFactor, so supersampled (DPR > 1) output uses screenshot capture.");
472
+ "ignores deviceScaleFactor, so scaled (DPR other than 1) output uses screenshot capture.");
473
473
  }
474
474
  if ((session.config?.useDrawElement ?? false) && !supersampling && forceScreenshot) {
475
475
  session.deGateReason = "render_mode_hint";
@@ -769,8 +769,8 @@ export async function createCaptureSession(serverUrl, outputDir, options, onBefo
769
769
  // (the captured surface is sized by the OS window in CSS pixels regardless
770
770
  // of `Emulation.setDeviceMetricsOverride`'s DPR). When supersampling we
771
771
  // need explicit clip+scale on `Page.captureScreenshot`, so fall back to
772
- // the screenshot path for any DPR > 1.
773
- const supersampling = (options.deviceScaleFactor ?? 1) > 1;
772
+ // the screenshot path for any DPR other than 1.
773
+ const supersampling = (options.deviceScaleFactor ?? 1) !== 1;
774
774
  const requestedGpuMode = config?.browserGpuMode ?? DEFAULT_CONFIG.browserGpuMode;
775
775
  const resolvedGpuMode = await resolveBrowserGpuMode(requestedGpuMode, {
776
776
  chromePath: headlessShell ?? undefined,
@@ -1529,11 +1529,12 @@ async function decodeAllImages(page) {
1529
1529
  }));
1530
1530
  });
1531
1531
  }
1532
- async function applyVideoMetadataHints(page, hints) {
1532
+ export async function applyVideoMetadataHints(page, hints) {
1533
1533
  if (!hints || hints.length === 0)
1534
1534
  return;
1535
1535
  // fallow-ignore-next-line complexity
1536
- await page.evaluate((metadataHints) => {
1536
+ await page.evaluate(async (metadataHints) => {
1537
+ const posters = new Set();
1537
1538
  for (const hint of metadataHints) {
1538
1539
  if (!hint.id ||
1539
1540
  !Number.isFinite(hint.width) ||
@@ -1544,18 +1545,18 @@ async function applyVideoMetadataHints(page, hints) {
1544
1545
  }
1545
1546
  const video = (window.__hfMediaEl?.(hint.id) ??
1546
1547
  document.getElementById(hint.id));
1547
- if (!video)
1548
+ if (!video || video.hasAttribute("poster"))
1548
1549
  continue;
1549
- if (!video.hasAttribute("width"))
1550
- video.setAttribute("width", String(hint.width));
1551
- if (!video.hasAttribute("height"))
1552
- video.setAttribute("height", String(hint.height));
1553
- const computed = window.getComputedStyle(video);
1554
- if (!video.style.aspectRatio &&
1555
- (!computed.aspectRatio || computed.aspectRatio === "auto")) {
1556
- video.style.aspectRatio = `${hint.width} / ${hint.height}`;
1557
- }
1550
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${hint.width}" height="${hint.height}"/>`;
1551
+ const intrinsicSizePoster = `data:image/svg+xml,${encodeURIComponent(svg)}`;
1552
+ video.poster = intrinsicSizePoster;
1553
+ posters.add(intrinsicSizePoster);
1558
1554
  }
1555
+ await Promise.all([...posters].map((src) => {
1556
+ const image = new Image();
1557
+ image.src = src;
1558
+ return image.decode();
1559
+ }));
1559
1560
  }, [...hints]);
1560
1561
  }
1561
1562
  async function waitForOptionalTailwindReady(page, timeoutMs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.167",
3
+ "version": "0.8.169",
4
4
  "description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -50,8 +50,8 @@
50
50
  "linkedom": "^0.18.12",
51
51
  "puppeteer": "^25.8.0",
52
52
  "puppeteer-core": "^25.8.0",
53
- "@hyperframes/parsers": "^0.8.167",
54
- "@hyperframes/core": "^0.8.167"
53
+ "@hyperframes/core": "^0.8.169",
54
+ "@hyperframes/parsers": "^0.8.169"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.0.10",