@hyperframes/engine 0.8.184 → 0.8.186

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.d.ts CHANGED
@@ -58,6 +58,7 @@ export { createFileServer, type FileServerOptions, type FileServerHandle, } from
58
58
  export { quantizeTimeToFrame, MEDIA_VISUAL_STYLE_PROPERTIES } from "@hyperframes/core";
59
59
  export { frameFileExtension } from "./services/frameCapture.js";
60
60
  export type { MotionBlurOptions, MotionBlurBlendSpace } from "./services/motionBlur.js";
61
+ export type { PageMotionBlurOff } from "./types.js";
61
62
  export { assertSwiftShader, readWebGlVendorInfo, SwiftShaderAssertionError, BROWSER_GPU_NOT_SOFTWARE, } from "./utils/assertSwiftShader.js";
62
63
  export { readWebGlVendorInfoFromCanvas } from "./utils/readWebGlVendorInfoFromCanvas.js";
63
64
  export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, probeMediaProfile, analyzeKeyframeIntervals, type VideoMetadata, type AudioMetadata, type MediaProbeProfile, type KeyframeAnalysis, } from "./utils/ffprobe.js";
@@ -364,11 +364,7 @@ async function probeBeginFrameSupport(browser, timeoutMs = BEGINFRAME_PROBE_TIME
364
364
  };
365
365
  }
366
366
  if (page && !(await settleWithin(page.close(), BEGINFRAME_PROBE_CLEANUP_TIMEOUT_MS))) {
367
- return {
368
- supported: false,
369
- detail: `${result.detail}; probe page cleanup timed out`,
370
- durationMs: Date.now() - started,
371
- };
367
+ return { ...result, detail: `${result.detail}; probe page cleanup timed out` };
372
368
  }
373
369
  return result;
374
370
  }
@@ -13,6 +13,7 @@ import { COMPOSITION_SOURCE_URL, quantizeSeekTime, quantizeTimeToFrame, fpsToNum
13
13
  import { DrawElementCaptureError } from "./drawElementCaptureError.js";
14
14
  import { encodePng } from "../utils/alphaBlit.js";
15
15
  import { DEFAULT_SAMPLES_PER_FRAME, MotionBlurAccumulator, SPATIAL_TWEEN_PROPERTIES, adaptiveSampleCount, motionBlurProbeTimes, motionBlurSampleTimes, motionBlurWindowIsStatic, probeDiffMagnitude, resolveMotionBlurPlan, } from "./motionBlur.js";
16
+ import { describePageMotionBlur, describePageMotionBlurAtEnd, installPageMotionBlur, } from "./pageMotionBlur.js";
16
17
  // ── Extracted modules ───────────────────────────────────────────────────────
17
18
  import { acquireBrowser, releaseBrowser, forceReleaseBrowser, buildChromeArgs, resolveBrowserGpuMode, captureWithoutWaylandDisplay, resolveHeadlessShellPath, compositionRequiresWebGpu, assertWebGpuAdapterAvailable, usesSoftwareWebGpu, } from "./browserManager.js";
18
19
  import { beginFrameCapture, ensureRenderFrameSiblings, getCdpSession, pageContentExceedsCaptureHeight, pageScreenshotCapture, initTransparentBackground, shouldDefaultCaptureBeyondViewport, } from "./screenshotService.js";
@@ -946,6 +947,7 @@ async function constructCaptureSession(input) {
946
947
  }
947
948
  }, variablesJson);
948
949
  }
950
+ await installPageMotionBlur(page, options);
949
951
  const browserVersion = await browser.version();
950
952
  const sessionOptions = resolveCaptureSessionOptions(options, browserVersion);
951
953
  const expectedMajor = config?.expectedChromiumMajor;
@@ -1498,8 +1500,8 @@ export function buildLiveMapWarning(libraries) {
1498
1500
  message: `Live map viewport(s) detected in the composition (${libraries.join(", ")}). ` +
1499
1501
  `Map tiles load over the network at render time, which the deterministic-render ` +
1500
1502
  `contract forbids: frames captured before tiles arrive ship a blank or partial map ` +
1501
- `with no error. Bake the map to a video first (see the motion-graphics maps skill / ` +
1502
- `bake-basemap.mjs) and use the baked file as the imagery layer.`,
1503
+ `with no error. Bake the map to a video first (the hyperframes skill's ` +
1504
+ `scripts/bake-basemap.mjs, see references/film.md) and use the baked file as the imagery layer.`,
1503
1505
  details: { sources: [...libraries] },
1504
1506
  };
1505
1507
  }
@@ -2462,6 +2464,7 @@ export async function createStaticVerificationPage(session) {
2462
2464
  JSON.parse(json);
2463
2465
  }, variablesJson);
2464
2466
  }
2467
+ await installPageMotionBlur(page, session.options);
2465
2468
  await page.setViewport({
2466
2469
  width: session.options.width,
2467
2470
  height: session.options.height,
@@ -3192,6 +3195,11 @@ async function captureFrameCore(session, frameIndex, time) {
3192
3195
  session.capturePerf.screenshotMs += screenshotMs;
3193
3196
  session.capturePerf.totalMs += captureTimeMs;
3194
3197
  session.capturePerf.frameMs.push(captureTimeMs);
3198
+ if (session.capturePerf.frames === 1) {
3199
+ const blur = await describePageMotionBlur(session.page).catch(() => null);
3200
+ if (blur)
3201
+ console.log(blur);
3202
+ }
3195
3203
  // Retain this freshly-captured buffer so the following static frames can reuse it.
3196
3204
  if (session.staticFrames) {
3197
3205
  session.lastFrameBuffer = buffer;
@@ -3527,6 +3535,11 @@ export async function closeCaptureSession(session) {
3527
3535
  // point up to `intervalMs` earlier. Best effort; the page may be gone.
3528
3536
  await session.chromeMemory.sampleOnce();
3529
3537
  }
3538
+ if (session.page && !session.pageReleased) {
3539
+ const blur = await describePageMotionBlurAtEnd(session.page);
3540
+ if (blur)
3541
+ console.log(blur);
3542
+ }
3530
3543
  // Realized static-dedup telemetry: how much the cache actually helped this
3531
3544
  // render (vs the prediction logged at arm time). Both capture paths
3532
3545
  // (sequential orchestrator + parallel workers) close their session here, so
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Tells the page's capture motion blur (`data-motion-blur="on"`) to stand down, before any of
3
+ * its scripts run: when the engine blurs (it would average frames that are already blurred) or
4
+ * when the render must match a Studio preview that could not blur (`pageMotionBlur: "off"`).
5
+ */
6
+ import type { Page } from "puppeteer-core";
7
+ import type { CaptureOptions } from "../types.js";
8
+ export declare function installPageMotionBlur(page: Pick<Page, "evaluateOnNewDocument">, options: Pick<CaptureOptions, "motionBlur" | "pageMotionBlur" | "deviceScaleFactor">): Promise<void>;
9
+ /** One line saying what the page's blur did for this session; null when the page never asked for blur. */
10
+ export declare function describePageMotionBlur(page: Pick<Page, "evaluate">): Promise<string | null>;
11
+ /** The session's closing line, only when frames fell back to plain DOM: the first-frame line cannot know. */
12
+ export declare function describePageMotionBlurAtEnd(page: Pick<Page, "evaluate">, timeoutMs?: number): Promise<string | null>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Tells the page's capture motion blur (`data-motion-blur="on"`) to stand down, before any of
3
+ * its scripts run: when the engine blurs (it would average frames that are already blurred) or
4
+ * when the render must match a Studio preview that could not blur (`pageMotionBlur: "off"`).
5
+ */
6
+ import { PAGE_DEVICE_SCALE_GLOBAL, PAGE_MOTION_BLUR_OFF_GLOBAL, acceleratedCanvasTrackerSource, } from "@hyperframes/core";
7
+ function pageOffReason(options) {
8
+ if (options.motionBlur)
9
+ return "engine-motion-blur";
10
+ if (options.pageMotionBlur === "off")
11
+ return "preview-unsupported";
12
+ return options.pageMotionBlur ?? null;
13
+ }
14
+ export async function installPageMotionBlur(page, options) {
15
+ await page.evaluateOnNewDocument(acceleratedCanvasTrackerSource());
16
+ const scale = options.deviceScaleFactor ?? 1;
17
+ if (scale > 1) {
18
+ await page.evaluateOnNewDocument((key, v) => {
19
+ window[key] = v;
20
+ }, PAGE_DEVICE_SCALE_GLOBAL, scale);
21
+ }
22
+ const off = pageOffReason(options);
23
+ if (!off)
24
+ return;
25
+ await page.evaluateOnNewDocument((key, v) => {
26
+ window[key] = v;
27
+ }, PAGE_MOTION_BLUR_OFF_GLOBAL, off);
28
+ }
29
+ async function readPageMotionBlur(page) {
30
+ return page.evaluate(() => {
31
+ const api = Reflect.get(window, "__hfMotionBlur");
32
+ const report = api && typeof api === "object" ? Reflect.get(api, "report") : null;
33
+ const got = typeof report === "function" ? report() : null;
34
+ if (!got || typeof got !== "object")
35
+ return null;
36
+ return {
37
+ state: String(Reflect.get(got, "state")),
38
+ code: String(Reflect.get(got, "reasonCode")),
39
+ movers: Number(Reflect.get(got, "movers")),
40
+ failedFrames: Number(Reflect.get(got, "failedFrames")) || 0,
41
+ };
42
+ });
43
+ }
44
+ const formatPageMotionBlur = (r) => `[motion-blur] ${r.state} reasonCode=${r.code} movers=${r.movers} failedFrames=${r.failedFrames}`;
45
+ /** One line saying what the page's blur did for this session; null when the page never asked for blur. */
46
+ export async function describePageMotionBlur(page) {
47
+ const r = await readPageMotionBlur(page);
48
+ return r && formatPageMotionBlur(r);
49
+ }
50
+ /** The session's closing line, only when frames fell back to plain DOM: the first-frame line cannot know. */
51
+ export async function describePageMotionBlurAtEnd(page, timeoutMs = 1500) {
52
+ let timer;
53
+ const stuck = new Promise((resolve) => {
54
+ timer = setTimeout(() => resolve(null), timeoutMs);
55
+ });
56
+ try {
57
+ const r = await Promise.race([readPageMotionBlur(page), stuck]);
58
+ return r && r.failedFrames > 0 ? `${formatPageMotionBlur(r)} (session end)` : null;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ finally {
64
+ clearTimeout(timer);
65
+ }
66
+ }
@@ -377,7 +377,7 @@ export function parseVideoElements(html) {
377
377
  mediaStart: readMediaStart(el),
378
378
  playbackRate: readElementRateSpec(el),
379
379
  loop: el.hasAttribute("loop"),
380
- hasAudio: hasAudioAttr === "true",
380
+ hasAudio: hasAudioAttr === "true" && isAudibleVideoElement(el),
381
381
  ...(isSelfOrAncestorHidden(el) ||
382
382
  (isAudibleVideoElement(el) && isMemberGroupHidden(groupsById, el))
383
383
  ? { hidden: true }
package/dist/types.d.ts CHANGED
@@ -8,6 +8,7 @@ import type { AudioFailureDiagnostic } from "./services/audioMixer.types.js";
8
8
  import type { Fps } from "@hyperframes/core";
9
9
  import type { ChromeMemoryStats } from "./services/chromeMemorySampler.js";
10
10
  import type { MotionBlurOptions } from "./services/motionBlur.js";
11
+ export type PageMotionBlurOff = "off" | "hdr-render" | "layered-composite" | "distributed-render";
11
12
  /**
12
13
  * Outcome of waiting for a sub-composition's GSAP timelines to register.
13
14
  * Threaded string-typed through `CapturePerfSummary` / `RenderPerfSummary` /
@@ -151,6 +152,8 @@ export interface CaptureOptions {
151
152
  * screenshot capture mode; see `services/motionBlur.ts`.
152
153
  */
153
154
  motionBlur?: MotionBlurOptions;
155
+ /** Why the page's capture motion blur stands down: the preview could not blur, or the route cannot host it. */
156
+ pageMotionBlur?: PageMotionBlurOff;
154
157
  deviceScaleFactor?: number;
155
158
  /**
156
159
  * Opt into Chrome's capture-beyond-viewport screenshot path. Leave undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.184",
3
+ "version": "0.8.186",
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/core": "^0.8.184",
54
- "@hyperframes/parsers": "^0.8.184"
53
+ "@hyperframes/core": "^0.8.186",
54
+ "@hyperframes/parsers": "^0.8.186"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.0.10",