@hyperframes/engine 0.6.119 → 0.6.121
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/package.json +24 -7
- package/scripts/generate-lut-reference.py +0 -168
- package/scripts/test-fitTextFontSize-browser.ts +0 -135
- package/src/cdp-headless-experimental.d.ts +0 -54
- package/src/config.test.ts +0 -213
- package/src/config.ts +0 -417
- package/src/index.ts +0 -273
- package/src/services/audioMixer.test.ts +0 -326
- package/src/services/audioMixer.ts +0 -604
- package/src/services/audioMixer.types.ts +0 -35
- package/src/services/audioVolumeEnvelope.test.ts +0 -176
- package/src/services/audioVolumeEnvelope.ts +0 -138
- package/src/services/browserManager.test.ts +0 -330
- package/src/services/browserManager.ts +0 -670
- package/src/services/chunkEncoder.test.ts +0 -1415
- package/src/services/chunkEncoder.ts +0 -831
- package/src/services/chunkEncoder.types.ts +0 -60
- package/src/services/extractionCache.test.ts +0 -199
- package/src/services/extractionCache.ts +0 -216
- package/src/services/fileServer.ts +0 -110
- package/src/services/frameCapture-discardWarmup.test.ts +0 -183
- package/src/services/frameCapture-namePolyfill.test.ts +0 -78
- package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
- package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
- package/src/services/frameCapture-warmupTicks.test.ts +0 -174
- package/src/services/frameCapture.test.ts +0 -192
- package/src/services/frameCapture.ts +0 -1934
- package/src/services/hdrCapture.test.ts +0 -159
- package/src/services/hdrCapture.ts +0 -315
- package/src/services/parallelCoordinator.test.ts +0 -139
- package/src/services/parallelCoordinator.ts +0 -437
- package/src/services/screenshotService.test.ts +0 -510
- package/src/services/screenshotService.ts +0 -615
- package/src/services/streamingEncoder.test.ts +0 -832
- package/src/services/streamingEncoder.ts +0 -594
- package/src/services/systemMemory.test.ts +0 -324
- package/src/services/systemMemory.ts +0 -180
- package/src/services/videoFrameExtractor.test.ts +0 -1062
- package/src/services/videoFrameExtractor.ts +0 -1139
- package/src/services/videoFrameInjector.test.ts +0 -300
- package/src/services/videoFrameInjector.ts +0 -687
- package/src/services/vp9Options.ts +0 -13
- package/src/types.ts +0 -191
- package/src/utils/alphaBlit.test.ts +0 -1349
- package/src/utils/alphaBlit.ts +0 -1015
- package/src/utils/assertSwiftShader.test.ts +0 -130
- package/src/utils/assertSwiftShader.ts +0 -126
- package/src/utils/ffmpegBinaries.test.ts +0 -43
- package/src/utils/ffmpegBinaries.ts +0 -63
- package/src/utils/ffprobe.test.ts +0 -342
- package/src/utils/ffprobe.ts +0 -457
- package/src/utils/gpuEncoder.test.ts +0 -140
- package/src/utils/gpuEncoder.ts +0 -268
- package/src/utils/hdr.test.ts +0 -191
- package/src/utils/hdr.ts +0 -137
- package/src/utils/hdrCompositing.test.ts +0 -130
- package/src/utils/htmlTemplate.test.ts +0 -42
- package/src/utils/htmlTemplate.ts +0 -42
- package/src/utils/layerCompositor.test.ts +0 -150
- package/src/utils/layerCompositor.ts +0 -58
- package/src/utils/parityContract.ts +0 -1
- package/src/utils/processTracker.test.ts +0 -74
- package/src/utils/processTracker.ts +0 -41
- package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
- package/src/utils/runFfmpeg.test.ts +0 -102
- package/src/utils/runFfmpeg.ts +0 -136
- package/src/utils/shaderTransitions.test.ts +0 -738
- package/src/utils/shaderTransitions.ts +0 -1130
- package/src/utils/uint16-alignment-audit.test.ts +0 -125
- package/src/utils/urlDownloader.test.ts +0 -65
- package/src/utils/urlDownloader.ts +0 -143
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -7
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
// @vitest-environment node
|
|
2
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
3
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { type Page } from "puppeteer-core";
|
|
7
|
-
|
|
8
|
-
// Hoist mocks before importing the module under test so the mock factory wins.
|
|
9
|
-
// The cache-hygiene block exercises createVideoFrameInjector against stubbed
|
|
10
|
-
// page-side primitives so we can assert on Node-side state (cache poisoning)
|
|
11
|
-
// without standing up a real browser.
|
|
12
|
-
const { injectVideoFramesBatchMock, syncVideoFrameVisibilityMock } = vi.hoisted(() => ({
|
|
13
|
-
injectVideoFramesBatchMock: vi.fn<
|
|
14
|
-
(page: Page, updates: Array<{ videoId: string; dataUri: string }>) => Promise<string[]>
|
|
15
|
-
>(async (_page, updates) => updates.map((u) => u.videoId)),
|
|
16
|
-
syncVideoFrameVisibilityMock: vi.fn<(page: Page, ids: string[]) => Promise<void>>(
|
|
17
|
-
async () => undefined,
|
|
18
|
-
),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
vi.mock("./screenshotService.js", () => ({
|
|
22
|
-
injectVideoFramesBatch: injectVideoFramesBatchMock,
|
|
23
|
-
syncVideoFrameVisibility: syncVideoFrameVisibilityMock,
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
import { __testing, createVideoFrameInjector } from "./videoFrameInjector.js";
|
|
27
|
-
import { type FrameLookupTable } from "./videoFrameExtractor.js";
|
|
28
|
-
import { DEFAULT_CONFIG } from "../config.js";
|
|
29
|
-
|
|
30
|
-
const { createFrameSourceCache } = __testing;
|
|
31
|
-
|
|
32
|
-
const SHARED_STATS = { evictions: 0, oversizedRejections: 0 };
|
|
33
|
-
|
|
34
|
-
describe("frame source cache eviction", () => {
|
|
35
|
-
let dir: string;
|
|
36
|
-
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
dir = mkdtempSync(join(tmpdir(), "hf-frame-cache-test-"));
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterEach(() => {
|
|
42
|
-
rmSync(dir, { recursive: true, force: true });
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Each PNG is base64-encoded into the data URI, so the cached string is
|
|
46
|
-
// ~4/3 the file size plus a small `data:image/png;base64,` prefix. Build
|
|
47
|
-
// distinct files so eviction has predictable victims.
|
|
48
|
-
function writeFrame(name: string, sizeBytes: number): string {
|
|
49
|
-
const filePath = join(dir, name);
|
|
50
|
-
writeFileSync(filePath, Buffer.alloc(sizeBytes, 0));
|
|
51
|
-
return filePath;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
it("evicts oldest entry when entry count exceeds limit", async () => {
|
|
55
|
-
const cache = createFrameSourceCache(2, Number.MAX_SAFE_INTEGER);
|
|
56
|
-
const a = writeFrame("a.png", 16);
|
|
57
|
-
const b = writeFrame("b.png", 16);
|
|
58
|
-
const c = writeFrame("c.png", 16);
|
|
59
|
-
|
|
60
|
-
await cache.get(a);
|
|
61
|
-
await cache.get(b);
|
|
62
|
-
expect(cache.stats().entries).toBe(2);
|
|
63
|
-
|
|
64
|
-
await cache.get(c);
|
|
65
|
-
expect(cache.stats().entries).toBe(2);
|
|
66
|
-
expect(cache.stats().evictions).toBe(1);
|
|
67
|
-
|
|
68
|
-
// Verify the *oldest* entry (a) was the victim — the LRU contract.
|
|
69
|
-
// A later get(a) is a miss-then-insert, which would also evict whichever
|
|
70
|
-
// entry is now oldest. We instrument the eviction counter to detect it.
|
|
71
|
-
const evictionsBefore = cache.stats().evictions;
|
|
72
|
-
await cache.get(a);
|
|
73
|
-
expect(cache.stats().evictions).toBe(evictionsBefore + 1);
|
|
74
|
-
// After re-inserting `a`, `b` is the next oldest. `c` is now newest.
|
|
75
|
-
// Touch `b` (move-to-front) → next eviction would be `c`, not `b`.
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("evicts oldest entry when byte budget is exceeded", async () => {
|
|
79
|
-
// 1 KB raw frame → ~1.4 KB base64 + ~22-byte data URI prefix. Pick a
|
|
80
|
-
// budget that comfortably fits two URIs but not three, so the third
|
|
81
|
-
// get() forces eviction even though the entry-count cap (100) is far
|
|
82
|
-
// from the limit.
|
|
83
|
-
const cache = createFrameSourceCache(100, 4 * 1024);
|
|
84
|
-
const a = writeFrame("a.png", 1024);
|
|
85
|
-
const b = writeFrame("b.png", 1024);
|
|
86
|
-
const c = writeFrame("c.png", 1024);
|
|
87
|
-
|
|
88
|
-
await cache.get(a);
|
|
89
|
-
await cache.get(b);
|
|
90
|
-
expect(cache.stats().entries).toBe(2);
|
|
91
|
-
|
|
92
|
-
await cache.get(c);
|
|
93
|
-
const afterC = cache.stats();
|
|
94
|
-
// The byte budget is the contract — the cache MUST stay under it after
|
|
95
|
-
// an insert that would otherwise overflow. Entry count is incidental.
|
|
96
|
-
expect(afterC.bytes).toBeLessThanOrEqual(4 * 1024);
|
|
97
|
-
expect(afterC.entries).toBeLessThan(3);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("returns the served URL untouched when frameSrcResolver yields one", async () => {
|
|
101
|
-
let served: string | null = "/served/frame.png";
|
|
102
|
-
const cache = createFrameSourceCache(4, 64 * 1024, () => served);
|
|
103
|
-
const file = writeFrame("a.png", 256);
|
|
104
|
-
|
|
105
|
-
expect(await cache.get(file)).toBe("/served/frame.png");
|
|
106
|
-
// Cache stays empty because the resolver short-circuits the read.
|
|
107
|
-
expect(cache.stats()).toMatchObject({ entries: 0, bytes: 0 });
|
|
108
|
-
|
|
109
|
-
served = null;
|
|
110
|
-
const dataUri = await cache.get(file);
|
|
111
|
-
expect(dataUri.startsWith("data:image/png;base64,")).toBe(true);
|
|
112
|
-
expect(cache.stats().entries).toBe(1);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("treats a re-read as a cache hit (no second file read)", async () => {
|
|
116
|
-
const cache = createFrameSourceCache(2, Number.MAX_SAFE_INTEGER);
|
|
117
|
-
const a = writeFrame("a.png", 64);
|
|
118
|
-
|
|
119
|
-
const first = await cache.get(a);
|
|
120
|
-
const second = await cache.get(a);
|
|
121
|
-
expect(second).toBe(first);
|
|
122
|
-
expect(cache.stats().entries).toBe(1);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("skips caching an entry that alone exceeds the byte budget (no self-eviction)", async () => {
|
|
126
|
-
// 64 KB raw → ~88 KB base64 + prefix. Budget of 32 KB rejects this entry.
|
|
127
|
-
// The contract: caller still gets the data URI; cache stays empty so
|
|
128
|
-
// future inserts aren't blocked by the rejected entry's bookkeeping.
|
|
129
|
-
const cache = createFrameSourceCache(100, 32 * 1024);
|
|
130
|
-
const big = writeFrame("big.png", 64 * 1024);
|
|
131
|
-
|
|
132
|
-
const dataUri = await cache.get(big);
|
|
133
|
-
expect(dataUri.startsWith("data:image/png;base64,")).toBe(true);
|
|
134
|
-
expect(cache.stats().entries).toBe(0);
|
|
135
|
-
expect(cache.stats().bytes).toBe(0);
|
|
136
|
-
expect(cache.stats().oversizedRejections).toBe(1);
|
|
137
|
-
expect(cache.stats().evictions).toBe(0);
|
|
138
|
-
|
|
139
|
-
// A subsequent normal-sized entry must cache cleanly — the rejection
|
|
140
|
-
// path didn't pollute internal state.
|
|
141
|
-
const small = writeFrame("small.png", 1024);
|
|
142
|
-
await cache.get(small);
|
|
143
|
-
expect(cache.stats().entries).toBe(1);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("at the production default (1500 MB), 1080p frames stay cached", async () => {
|
|
147
|
-
// Regression for the post-PR-#662 default: previously the cache held up
|
|
148
|
-
// to 256 entries × ~8 MB ≈ 2 GB at 1080p. The new byte-budget default of
|
|
149
|
-
// 1500 MB caps it tighter (~187 entries at 1080p ≈ 6s @ 30fps). This
|
|
150
|
-
// test pins the math so a future tweak to the default is visible.
|
|
151
|
-
const oneEightyP_jpegSize = 8 * 1024 * 1024; // ~8 MB JPEG (data URI)
|
|
152
|
-
const defaultBytesLimit = DEFAULT_CONFIG.frameDataUriCacheBytesLimitMb * 1024 * 1024;
|
|
153
|
-
const expectedMaxEntries = Math.floor(defaultBytesLimit / oneEightyP_jpegSize);
|
|
154
|
-
expect(expectedMaxEntries).toBeGreaterThanOrEqual(180);
|
|
155
|
-
expect(expectedMaxEntries).toBeLessThanOrEqual(200);
|
|
156
|
-
// At 30fps that's at least 6 seconds of look-ahead. Sequential access is
|
|
157
|
-
// strictly cheaper, so the cache helps any seek-back ≤ 6s.
|
|
158
|
-
expect(expectedMaxEntries / 30).toBeGreaterThanOrEqual(6);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// Suppress unused-import warning when the SHARED_STATS sentinel is dropped.
|
|
162
|
-
it("stats() exposes counters used by telemetry", async () => {
|
|
163
|
-
const cache = createFrameSourceCache(1, Number.MAX_SAFE_INTEGER);
|
|
164
|
-
expect(cache.stats()).toMatchObject({ ...SHARED_STATS, entries: 0, bytes: 0 });
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe("createVideoFrameInjector cache hygiene against page-side skips", () => {
|
|
169
|
-
// Build a minimal FrameLookupTable stand-in that returns one fixed payload
|
|
170
|
-
// for every time so we can drive the hook deterministically. The real
|
|
171
|
-
// table is exercised exhaustively in videoFrameExtractor.test.ts.
|
|
172
|
-
function fakeTable(payload: { videoId: string; framePath: string; frameIndex: number }) {
|
|
173
|
-
return {
|
|
174
|
-
getActiveFramePayloads: () =>
|
|
175
|
-
new Map([
|
|
176
|
-
[payload.videoId, { framePath: payload.framePath, frameIndex: payload.frameIndex }],
|
|
177
|
-
]),
|
|
178
|
-
} as unknown as FrameLookupTable;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Bypass the on-disk frame cache by handing back a synthetic data URI.
|
|
182
|
-
function inlineResolver(framePath: string): string {
|
|
183
|
-
return `data:image/png;base64,fake-${framePath}`;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
beforeEach(() => {
|
|
187
|
-
injectVideoFramesBatchMock.mockReset();
|
|
188
|
-
syncVideoFrameVisibilityMock.mockReset();
|
|
189
|
-
syncVideoFrameVisibilityMock.mockResolvedValue(undefined);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("does not poison the lastInjected cache when the page reports zero ids injected", async () => {
|
|
193
|
-
// Regression for the agentic-finecut scenario after PR #1028's ancestor
|
|
194
|
-
// skip: when injectVideoFramesBatch silently drops a video (its sub-comp
|
|
195
|
-
// host is hidden), the caller used to record `lastInjectedFrame[v] = N`
|
|
196
|
-
// anyway. On the next frame, if the source frameIndex is unchanged
|
|
197
|
-
// (low-fps source, multiple output frames per source frame, or
|
|
198
|
-
// non-frame-aligned host start), the cache short-circuits the second
|
|
199
|
-
// call and the host's first visible frame paints blank because the
|
|
200
|
-
// replacement <img> was never created.
|
|
201
|
-
//
|
|
202
|
-
// Pin the contract: when the page returns `[]` (no ids actually
|
|
203
|
-
// injected), the cache must not record those frameIndexes, so a follow-
|
|
204
|
-
// up call at the same frameIndex still issues an inject.
|
|
205
|
-
// The injector calls page.evaluate after injecting frames (GPU reseek);
|
|
206
|
-
// stub it so these cache-hygiene cases exercise the real code path.
|
|
207
|
-
const fakePage = { evaluate: async () => undefined } as unknown as Page;
|
|
208
|
-
const hook = createVideoFrameInjector(
|
|
209
|
-
fakeTable({ videoId: "pip", framePath: "/p", frameIndex: 5 }),
|
|
210
|
-
{
|
|
211
|
-
frameSrcResolver: inlineResolver,
|
|
212
|
-
},
|
|
213
|
-
);
|
|
214
|
-
expect(hook).not.toBeNull();
|
|
215
|
-
|
|
216
|
-
// First call: simulate the ancestor-hidden skip — page-side reports it
|
|
217
|
-
// injected nothing.
|
|
218
|
-
injectVideoFramesBatchMock.mockResolvedValueOnce([]);
|
|
219
|
-
await hook!(fakePage, 0);
|
|
220
|
-
expect(injectVideoFramesBatchMock).toHaveBeenCalledTimes(1);
|
|
221
|
-
expect(injectVideoFramesBatchMock).toHaveBeenLastCalledWith(fakePage, [
|
|
222
|
-
{ videoId: "pip", dataUri: "data:image/png;base64,fake-/p" },
|
|
223
|
-
]);
|
|
224
|
-
|
|
225
|
-
// Second call: same frameIndex, but the previous call did not really
|
|
226
|
-
// paint. The cache must NOT short-circuit; the inject must run again.
|
|
227
|
-
injectVideoFramesBatchMock.mockResolvedValueOnce(["pip"]);
|
|
228
|
-
await hook!(fakePage, 0);
|
|
229
|
-
expect(injectVideoFramesBatchMock).toHaveBeenCalledTimes(2);
|
|
230
|
-
expect(injectVideoFramesBatchMock).toHaveBeenLastCalledWith(fakePage, [
|
|
231
|
-
{ videoId: "pip", dataUri: "data:image/png;base64,fake-/p" },
|
|
232
|
-
]);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("does cache normally when the page reports the id as injected", async () => {
|
|
236
|
-
// Counter-test: when injection succeeds for a videoId, the cache must
|
|
237
|
-
// record it and a second call at the same frameIndex must short-circuit.
|
|
238
|
-
// This pins the happy path so a future refactor can't trade the skip
|
|
239
|
-
// bug for a never-cache regression.
|
|
240
|
-
// The injector calls page.evaluate after injecting frames (GPU reseek);
|
|
241
|
-
// stub it so these cache-hygiene cases exercise the real code path.
|
|
242
|
-
const fakePage = { evaluate: async () => undefined } as unknown as Page;
|
|
243
|
-
const hook = createVideoFrameInjector(
|
|
244
|
-
fakeTable({ videoId: "pip", framePath: "/p", frameIndex: 5 }),
|
|
245
|
-
{
|
|
246
|
-
frameSrcResolver: inlineResolver,
|
|
247
|
-
},
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
injectVideoFramesBatchMock.mockResolvedValueOnce(["pip"]);
|
|
251
|
-
await hook!(fakePage, 0);
|
|
252
|
-
expect(injectVideoFramesBatchMock).toHaveBeenCalledTimes(1);
|
|
253
|
-
|
|
254
|
-
await hook!(fakePage, 0);
|
|
255
|
-
// Cache hit — no second inject for the same frameIndex.
|
|
256
|
-
expect(injectVideoFramesBatchMock).toHaveBeenCalledTimes(1);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Regression: WebGL/WebGPU compositions that sample a <video> as a texture
|
|
260
|
-
// render on `hf-seek` BEFORE frames are injected. After injecting the
|
|
261
|
-
// decoded frames, the hook must re-render the GPU adapters at the same time
|
|
262
|
-
// (window.__hfReseekGpu) so they re-upload their textures from the fresh
|
|
263
|
-
// frames — otherwise the facet flickers / goes black non-deterministically.
|
|
264
|
-
it("re-renders GPU adapters after injecting frames (post-injection reseek)", async () => {
|
|
265
|
-
const evaluate = vi.fn(async () => undefined);
|
|
266
|
-
const page = { evaluate } as unknown as Page;
|
|
267
|
-
const hook = createVideoFrameInjector(
|
|
268
|
-
fakeTable({ videoId: "facet", framePath: "/f", frameIndex: 3 }),
|
|
269
|
-
{ frameSrcResolver: inlineResolver },
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
injectVideoFramesBatchMock.mockResolvedValueOnce(["facet"]);
|
|
273
|
-
await hook!(page, 1.5);
|
|
274
|
-
|
|
275
|
-
const reseekCall = evaluate.mock.calls.find((call) => call[1] === 1.5);
|
|
276
|
-
expect(reseekCall).toBeDefined();
|
|
277
|
-
// The evaluated page function invokes window.__hfReseekGpu(time).
|
|
278
|
-
const pageFn = reseekCall![0] as (t: number) => void;
|
|
279
|
-
const reseek = vi.fn();
|
|
280
|
-
(globalThis as unknown as { window?: unknown }).window = { __hfReseekGpu: reseek };
|
|
281
|
-
pageFn(1.5);
|
|
282
|
-
delete (globalThis as unknown as { window?: unknown }).window;
|
|
283
|
-
expect(reseek).toHaveBeenCalledWith(1.5);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("does not reseek GPU when the page injected no frames", async () => {
|
|
287
|
-
const evaluate = vi.fn(async () => undefined);
|
|
288
|
-
const page = { evaluate } as unknown as Page;
|
|
289
|
-
const hook = createVideoFrameInjector(
|
|
290
|
-
fakeTable({ videoId: "facet", framePath: "/f", frameIndex: 3 }),
|
|
291
|
-
{ frameSrcResolver: inlineResolver },
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
// Page dropped the video (e.g. hidden host) → nothing injected → no reseek.
|
|
295
|
-
injectVideoFramesBatchMock.mockResolvedValueOnce([]);
|
|
296
|
-
await hook!(page, 1.5);
|
|
297
|
-
|
|
298
|
-
expect(evaluate.mock.calls.some((call) => call[1] === 1.5)).toBe(false);
|
|
299
|
-
});
|
|
300
|
-
});
|