@hyperframes/engine 0.6.118 → 0.6.120

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.
Files changed (73) hide show
  1. package/package.json +24 -7
  2. package/scripts/generate-lut-reference.py +0 -168
  3. package/scripts/test-fitTextFontSize-browser.ts +0 -135
  4. package/src/cdp-headless-experimental.d.ts +0 -54
  5. package/src/config.test.ts +0 -213
  6. package/src/config.ts +0 -417
  7. package/src/index.ts +0 -273
  8. package/src/services/audioMixer.test.ts +0 -326
  9. package/src/services/audioMixer.ts +0 -604
  10. package/src/services/audioMixer.types.ts +0 -35
  11. package/src/services/audioVolumeEnvelope.test.ts +0 -176
  12. package/src/services/audioVolumeEnvelope.ts +0 -138
  13. package/src/services/browserManager.test.ts +0 -330
  14. package/src/services/browserManager.ts +0 -670
  15. package/src/services/chunkEncoder.test.ts +0 -1415
  16. package/src/services/chunkEncoder.ts +0 -831
  17. package/src/services/chunkEncoder.types.ts +0 -60
  18. package/src/services/extractionCache.test.ts +0 -199
  19. package/src/services/extractionCache.ts +0 -216
  20. package/src/services/fileServer.ts +0 -110
  21. package/src/services/frameCapture-discardWarmup.test.ts +0 -183
  22. package/src/services/frameCapture-namePolyfill.test.ts +0 -78
  23. package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
  24. package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
  25. package/src/services/frameCapture-warmupTicks.test.ts +0 -174
  26. package/src/services/frameCapture.test.ts +0 -192
  27. package/src/services/frameCapture.ts +0 -1934
  28. package/src/services/hdrCapture.test.ts +0 -159
  29. package/src/services/hdrCapture.ts +0 -315
  30. package/src/services/parallelCoordinator.test.ts +0 -139
  31. package/src/services/parallelCoordinator.ts +0 -437
  32. package/src/services/screenshotService.test.ts +0 -510
  33. package/src/services/screenshotService.ts +0 -615
  34. package/src/services/streamingEncoder.test.ts +0 -832
  35. package/src/services/streamingEncoder.ts +0 -594
  36. package/src/services/systemMemory.test.ts +0 -324
  37. package/src/services/systemMemory.ts +0 -180
  38. package/src/services/videoFrameExtractor.test.ts +0 -1062
  39. package/src/services/videoFrameExtractor.ts +0 -1139
  40. package/src/services/videoFrameInjector.test.ts +0 -300
  41. package/src/services/videoFrameInjector.ts +0 -687
  42. package/src/services/vp9Options.ts +0 -13
  43. package/src/types.ts +0 -191
  44. package/src/utils/alphaBlit.test.ts +0 -1349
  45. package/src/utils/alphaBlit.ts +0 -1015
  46. package/src/utils/assertSwiftShader.test.ts +0 -130
  47. package/src/utils/assertSwiftShader.ts +0 -126
  48. package/src/utils/ffmpegBinaries.test.ts +0 -43
  49. package/src/utils/ffmpegBinaries.ts +0 -63
  50. package/src/utils/ffprobe.test.ts +0 -342
  51. package/src/utils/ffprobe.ts +0 -457
  52. package/src/utils/gpuEncoder.test.ts +0 -140
  53. package/src/utils/gpuEncoder.ts +0 -268
  54. package/src/utils/hdr.test.ts +0 -191
  55. package/src/utils/hdr.ts +0 -137
  56. package/src/utils/hdrCompositing.test.ts +0 -130
  57. package/src/utils/htmlTemplate.test.ts +0 -42
  58. package/src/utils/htmlTemplate.ts +0 -42
  59. package/src/utils/layerCompositor.test.ts +0 -150
  60. package/src/utils/layerCompositor.ts +0 -58
  61. package/src/utils/parityContract.ts +0 -1
  62. package/src/utils/processTracker.test.ts +0 -74
  63. package/src/utils/processTracker.ts +0 -41
  64. package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
  65. package/src/utils/runFfmpeg.test.ts +0 -102
  66. package/src/utils/runFfmpeg.ts +0 -136
  67. package/src/utils/shaderTransitions.test.ts +0 -738
  68. package/src/utils/shaderTransitions.ts +0 -1130
  69. package/src/utils/uint16-alignment-audit.test.ts +0 -125
  70. package/src/utils/urlDownloader.test.ts +0 -65
  71. package/src/utils/urlDownloader.ts +0 -143
  72. package/tsconfig.json +0 -19
  73. 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
- });