@hyperframes/engine 0.8.216 → 0.8.218
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.
|
@@ -743,7 +743,7 @@ async function cssEffectRiskFrames(session, totalFrames) {
|
|
|
743
743
|
if (!scan.reason || !scan.bounded)
|
|
744
744
|
return null;
|
|
745
745
|
const fps = fpsToNumber(session.options.fps);
|
|
746
|
-
const live = await scanCssEffectLiveFrames(scratch.page, fps, totalFrames, totalFrames * MAX_RISK_FRAME_FRACTION);
|
|
746
|
+
const live = await scanCssEffectLiveFrames(scratch.page, fps, totalFrames, totalFrames * MAX_RISK_FRAME_FRACTION, { onProgress: session.options.onEffectScanProgress });
|
|
747
747
|
return live && withFramesAfterCallback(live, scan.callbackFrom, fps, totalFrames);
|
|
748
748
|
}
|
|
749
749
|
catch (error) {
|
|
@@ -14,11 +14,16 @@ export declare const RISK_RENDER_ADDED_BYTES = 1000000000;
|
|
|
14
14
|
export declare const RISK_RENDER_HEADROOM_BYTES = 1500000000;
|
|
15
15
|
/** Risk ranges closer than this many frames are joined, so capture never alternates modes. */
|
|
16
16
|
export declare const RISK_RANGE_HYSTERESIS_FRAMES = 12;
|
|
17
|
+
interface ScanOptions {
|
|
18
|
+
budgetMs?: number;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
onProgress?: (scannedFrames: number, totalFrames: number) => void;
|
|
21
|
+
}
|
|
17
22
|
/**
|
|
18
23
|
* Frames where a recorded effect element shows. Seeks the page forward, so use a throwaway one.
|
|
19
|
-
* Null once more than `maxFrames` frames qualify
|
|
24
|
+
* Null once more than `maxFrames` frames qualify, or once the whole scan would take over `budgetMs`.
|
|
20
25
|
*/
|
|
21
|
-
export declare function scanCssEffectLiveFrames(page: Page, fps: number, totalFrames: number, maxFrames: number): Promise<Set<number> | null>;
|
|
26
|
+
export declare function scanCssEffectLiveFrames(page: Page, fps: number, totalFrames: number, maxFrames: number, { budgetMs, now, onProgress }?: ScanOptions): Promise<Set<number> | null>;
|
|
22
27
|
/**
|
|
23
28
|
* The given frames with a one-frame margin, ranges closer than {@link RISK_RANGE_HYSTERESIS_FRAMES}
|
|
24
29
|
* joined. Null when they cover more than {@link MAX_RISK_FRAME_FRACTION} of the render.
|
|
@@ -28,3 +33,4 @@ export declare function planRiskFrames(frames: Iterable<number>, totalFrames: nu
|
|
|
28
33
|
export declare function withFramesAfterCallback(live: Set<number>, callbackFrom: number | null, fps: number, totalFrames: number): Set<number>;
|
|
29
34
|
/** Whether the render fits: what it adds, scaled by output pixels, leaves the headroom. */
|
|
30
35
|
export declare function riskSessionFits(availableMb: number, outputPixels: number): boolean;
|
|
36
|
+
export {};
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { quantizeTimeToFrame } from "@hyperframes/core";
|
|
6
6
|
/** Frames per page round trip while scanning; a scan is one in-page loop per chunk. */
|
|
7
7
|
const SCAN_CHUNK_FRAMES = 240;
|
|
8
|
+
/** Longest the scan may hold back the first frame; above it the whole render captures by screenshot. */
|
|
9
|
+
const SCAN_BUDGET_MS = 120_000;
|
|
8
10
|
/**
|
|
9
11
|
* Above this share of risk frames the second browser and the switching cost outweigh the fast
|
|
10
12
|
* frames, so the whole render captures by screenshot as it always has.
|
|
@@ -17,44 +19,52 @@ export const RISK_RENDER_HEADROOM_BYTES = 1_500_000_000;
|
|
|
17
19
|
const PIXELS_1080P = 1920 * 1080;
|
|
18
20
|
/** Risk ranges closer than this many frames are joined, so capture never alternates modes. */
|
|
19
21
|
export const RISK_RANGE_HYSTERESIS_FRAMES = 12;
|
|
22
|
+
async function scanChunk(page, times) {
|
|
23
|
+
return page.evaluate(async (ts) => {
|
|
24
|
+
const w = window;
|
|
25
|
+
const targets = w.__hfCssFxTargets ?? [];
|
|
26
|
+
// A hidden element still shows a descendant that sets `visibility: visible` itself.
|
|
27
|
+
const paints = (el) => getComputedStyle(el).visibility !== "hidden" ||
|
|
28
|
+
Array.from(el.querySelectorAll("*")).some((d) => getComputedStyle(d).visibility === "visible");
|
|
29
|
+
const shows = (el) => {
|
|
30
|
+
for (let n = el; n; n = n.parentElement) {
|
|
31
|
+
const cs = getComputedStyle(n);
|
|
32
|
+
if (cs.display === "none" || Number(cs.opacity) === 0)
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return paints(el);
|
|
36
|
+
};
|
|
37
|
+
const out = [];
|
|
38
|
+
for (let i = 0; i < ts.length; i++) {
|
|
39
|
+
w.__hf?.seek?.(ts[i]);
|
|
40
|
+
await w.__hfWaitForSeekCompletion?.();
|
|
41
|
+
if (targets.some(shows))
|
|
42
|
+
out.push(i);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}, times);
|
|
46
|
+
}
|
|
20
47
|
/**
|
|
21
48
|
* Frames where a recorded effect element shows. Seeks the page forward, so use a throwaway one.
|
|
22
|
-
* Null once more than `maxFrames` frames qualify
|
|
49
|
+
* Null once more than `maxFrames` frames qualify, or once the whole scan would take over `budgetMs`.
|
|
23
50
|
*/
|
|
24
|
-
export async function scanCssEffectLiveFrames(page, fps, totalFrames, maxFrames) {
|
|
51
|
+
export async function scanCssEffectLiveFrames(page, fps, totalFrames, maxFrames, { budgetMs = SCAN_BUDGET_MS, now = () => performance.now(), onProgress } = {}) {
|
|
25
52
|
const live = new Set();
|
|
26
|
-
|
|
53
|
+
const started = now();
|
|
54
|
+
for (let from = 0; from < totalFrames && live.size <= maxFrames; from += SCAN_CHUNK_FRAMES) {
|
|
55
|
+
const to = Math.min(totalFrames, from + SCAN_CHUNK_FRAMES);
|
|
27
56
|
const times = [];
|
|
28
|
-
for (let f = from; f <
|
|
57
|
+
for (let f = from; f < to; f++)
|
|
29
58
|
times.push(quantizeTimeToFrame(f / fps, fps));
|
|
30
|
-
|
|
31
|
-
const hits = await page.evaluate(async (ts) => {
|
|
32
|
-
const w = window;
|
|
33
|
-
const targets = w.__hfCssFxTargets ?? [];
|
|
34
|
-
// A hidden element still shows a descendant that sets `visibility: visible` itself.
|
|
35
|
-
const paints = (el) => getComputedStyle(el).visibility !== "hidden" ||
|
|
36
|
-
Array.from(el.querySelectorAll("*")).some((d) => getComputedStyle(d).visibility === "visible");
|
|
37
|
-
const shows = (el) => {
|
|
38
|
-
for (let n = el; n; n = n.parentElement) {
|
|
39
|
-
const cs = getComputedStyle(n);
|
|
40
|
-
if (cs.display === "none" || Number(cs.opacity) === 0)
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
return paints(el);
|
|
44
|
-
};
|
|
45
|
-
const out = [];
|
|
46
|
-
for (let i = 0; i < ts.length; i++) {
|
|
47
|
-
w.__hf?.seek?.(ts[i]);
|
|
48
|
-
await w.__hfWaitForSeekCompletion?.();
|
|
49
|
-
if (targets.some(shows))
|
|
50
|
-
out.push(i);
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
}, times);
|
|
54
|
-
for (const i of hits)
|
|
59
|
+
for (const i of await scanChunk(page, times))
|
|
55
60
|
live.add(from + i);
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
onProgress?.(to, totalFrames);
|
|
62
|
+
const projectedMs = ((now() - started) / to) * totalFrames;
|
|
63
|
+
if (to < totalFrames && projectedMs > budgetMs) {
|
|
64
|
+
console.error(`[engine] fast capture: the effect scan of ${totalFrames} frames would take ` +
|
|
65
|
+
`${Math.round(projectedMs / 1000)} s, over its ${budgetMs / 1000} s budget`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
58
68
|
}
|
|
59
69
|
return live.size > maxFrames ? null : live;
|
|
60
70
|
}
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.218",
|
|
4
4
|
"description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"puppeteer": "~25.11.0",
|
|
56
56
|
"puppeteer-core": "^25.11.0",
|
|
57
57
|
"undici": "7.25.0",
|
|
58
|
-
"@hyperframes/core": "^0.8.
|
|
59
|
-
"@hyperframes/parsers": "^0.8.
|
|
58
|
+
"@hyperframes/core": "^0.8.218",
|
|
59
|
+
"@hyperframes/parsers": "^0.8.218"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^25.0.10",
|