@hyperframes/studio 0.8.139 → 0.8.140

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 (72) hide show
  1. package/dist/assets/{hyperframes-player-CckOoCJ8.js → hyperframes-player-BLuyxnK7.js} +1 -1
  2. package/dist/assets/{index-DsEkE5_G.js → index-CWfuDCoA.js} +1 -1
  3. package/dist/assets/{index-BmIPrb8I.js → index-Dr3Bo9uH.js} +1 -1
  4. package/dist/assets/{index-Br1Whc0f.js → index-VAHDM_zL.js} +204 -204
  5. package/dist/{chunk-E422JDF5.js → chunk-H2DUOXTF.js} +2 -1
  6. package/dist/index.d.ts +19 -3
  7. package/dist/index.html +1 -1
  8. package/dist/index.js +6498 -5813
  9. package/dist/index.js.map +1 -1
  10. package/dist/player.js +1 -1
  11. package/package.json +8 -8
  12. package/src/components/TimelineToolbar.test.tsx +79 -1
  13. package/src/components/TimelineToolbar.tsx +14 -29
  14. package/src/components/editor/DomEditOverlay.tsx +2 -2
  15. package/src/components/editor/PropertyPanel.tsx +1 -0
  16. package/src/components/editor/PropertyPanelFlat.tsx +1 -0
  17. package/src/components/editor/manualEdits.ts +1 -1
  18. package/src/components/editor/manualEditsParsing.ts +14 -2
  19. package/src/components/editor/previewReadOnly.overlay.test.tsx +3 -2
  20. package/src/components/editor/propertyPanelFill.tsx +36 -13
  21. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +4 -0
  22. package/src/components/editor/propertyPanelFlatStyleSections.tsx +13 -7
  23. package/src/components/editor/propertyPanelImageFill.test.tsx +164 -0
  24. package/src/components/editor/propertyPanelStyleSections.tsx +10 -7
  25. package/src/components/nle/NLEPreview.test.ts +44 -1
  26. package/src/components/nle/NLEPreview.tsx +7 -2
  27. package/src/hooks/externalFileChangeOwnership.test.ts +25 -4
  28. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +3 -22
  29. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +536 -10
  30. package/src/hooks/useExternalFileChangeCoordinator.ts +72 -45
  31. package/src/hooks/useMusicBeatAnalysis.ts +2 -1
  32. package/src/hooks/useSdkSession.lifecycle.test.tsx +20 -0
  33. package/src/hooks/useSdkSession.ts +4 -1
  34. package/src/hooks/useThumbnailStripSize.test.tsx +65 -0
  35. package/src/hooks/useThumbnailStripSize.ts +23 -1
  36. package/src/index.ts +1 -0
  37. package/src/player/components/AudioWaveform.test.tsx +153 -1
  38. package/src/player/components/AudioWaveform.tsx +93 -37
  39. package/src/player/components/Timeline.scrollRest.test.tsx +22 -0
  40. package/src/player/components/Timeline.test.ts +3 -1
  41. package/src/player/components/TimelineCanvas.tsx +4 -0
  42. package/src/player/components/TimelineClip.tsx +20 -6
  43. package/src/player/components/TimelineClipFades.test.tsx +11 -0
  44. package/src/player/components/TimelineClipFades.tsx +4 -4
  45. package/src/player/components/TimelineClipJoins.tsx +23 -18
  46. package/src/player/components/TimelineGestureOverlay.tsx +30 -27
  47. package/src/player/components/TimelineGroupRow.tsx +1 -0
  48. package/src/player/components/TimelineLaneClip.tsx +127 -0
  49. package/src/player/components/TimelineLanes.test.tsx +51 -6
  50. package/src/player/components/TimelineLanes.tsx +166 -178
  51. package/src/player/components/TimelineRuler.tsx +2 -0
  52. package/src/player/components/TimelineTimeLayer.tsx +25 -0
  53. package/src/player/components/VideoThumbnail.test.tsx +204 -12
  54. package/src/player/components/VideoThumbnail.tsx +37 -9
  55. package/src/player/components/thumbnailUtils.test.ts +17 -3
  56. package/src/player/components/thumbnailUtils.ts +17 -1
  57. package/src/player/components/timelineLayout.test.ts +3 -3
  58. package/src/player/components/timelineMotion.test.ts +27 -0
  59. package/src/player/components/timelineMotion.ts +62 -0
  60. package/src/player/components/timelineZoomInput.test.ts +438 -0
  61. package/src/player/components/timelineZoomInput.ts +456 -0
  62. package/src/player/components/useTimelineGeometry.ts +0 -3
  63. package/src/player/components/useTimelinePlayhead.test.tsx +97 -30
  64. package/src/player/components/useTimelinePlayhead.ts +53 -59
  65. package/src/player/components/useTimelineProviderState.tsx +16 -21
  66. package/src/player/components/useTimelineZoom.ts +7 -12
  67. package/src/player/lib/thumbnailVideoDecoder.test.ts +230 -30
  68. package/src/player/lib/thumbnailVideoDecoder.ts +197 -54
  69. package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
  70. package/src/player/lib/timelineViewportBudgets.ts +1 -1
  71. package/src/utils/gsapUndoRestore.browser.test.ts +4 -17
  72. /package/dist/{chunk-E422JDF5.js.map → chunk-H2DUOXTF.js.map} +0 -0
@@ -1,14 +1,21 @@
1
1
  import { TIMELINE_VIEWPORT_BUDGETS, type TimelineViewportBudgets } from "./timelineViewportBudgets";
2
- import type { ThumbnailLoadedResult, ThumbnailValue } from "./thumbnailScheduler";
2
+ import {
3
+ createThumbnailKey,
4
+ type ThumbnailLoadedResult,
5
+ type ThumbnailValue,
6
+ } from "./thumbnailScheduler";
3
7
 
4
8
  export interface VideoThumbnailDecodeRequest {
5
9
  source: string;
10
+ contentVersion?: string;
6
11
  sourceStart?: number;
7
12
  sourceRangeDuration?: number;
8
13
  frameCount: number;
9
14
  fit?: "contain" | "cover";
10
15
  }
11
16
 
17
+ const LAST_FRAME_INSET_S = 0.001;
18
+
12
19
  export function videoThumbnailTimestamps(
13
20
  start: number,
14
21
  duration: number,
@@ -18,10 +25,63 @@ export function videoThumbnailTimestamps(
18
25
  const safeDuration = Math.max(0, Number.isFinite(duration) ? duration : 0);
19
26
  const count = Math.max(1, Number.isFinite(frameCount) ? Math.floor(frameCount) : 1);
20
27
  if (count === 1) return [safeStart + safeDuration / 2];
21
- return Array.from(
28
+ const edges = Array.from(
22
29
  { length: count },
23
- (_, index) => safeStart + (safeDuration * index) / (count - 1),
30
+ (_, index) => safeStart + (safeDuration * index) / count,
24
31
  );
32
+ return [...edges, safeStart + Math.max(0, safeDuration - LAST_FRAME_INSET_S)];
33
+ }
34
+
35
+ interface SharedFrame {
36
+ url: string;
37
+ decodedAt: number;
38
+ users: number;
39
+ }
40
+
41
+ interface HeldFrame {
42
+ key: string;
43
+ frame: SharedFrame;
44
+ }
45
+
46
+ interface FrameWindow {
47
+ sourceStart: number;
48
+ lead: number;
49
+ end: number;
50
+ }
51
+
52
+ const sharedFrames = new Map<string, SharedFrame[]>();
53
+ const sourceInfos = new Map<string, { version: string | undefined; info: SourceInfo }>();
54
+
55
+ const accepts = (window: FrameWindow, time: number, decodedAt: number) =>
56
+ decodedAt >= window.sourceStart && time - decodedAt <= (time >= window.end ? 0 : window.lead);
57
+
58
+ function takeSharedFrame(key: string, time: number, window: FrameWindow): SharedFrame | undefined {
59
+ const frame = sharedFrames.get(key)?.find((shared) => accepts(window, time, shared.decodedAt));
60
+ if (frame) frame.users += 1;
61
+ return frame;
62
+ }
63
+
64
+ function shareFrame(key: string, decodedAt: number, url: string): SharedFrame {
65
+ const frames = sharedFrames.get(key) ?? [];
66
+ const existing = frames.find((shared) => shared.decodedAt === decodedAt);
67
+ if (existing) {
68
+ existing.users += 1;
69
+ URL.revokeObjectURL(url);
70
+ return existing;
71
+ }
72
+ const frame = { url, decodedAt, users: 1 };
73
+ sharedFrames.set(key, [...frames, frame]);
74
+ return frame;
75
+ }
76
+
77
+ function releaseSharedFrames(held: HeldFrame[]): void {
78
+ for (const { key, frame } of held.splice(0)) {
79
+ if (--frame.users > 0) continue;
80
+ const rest = sharedFrames.get(key)?.filter((shared) => shared !== frame) ?? [];
81
+ if (rest.length > 0) sharedFrames.set(key, rest);
82
+ else sharedFrames.delete(key);
83
+ URL.revokeObjectURL(frame.url);
84
+ }
25
85
  }
26
86
 
27
87
  async function canvasToBlob(canvas: HTMLCanvasElement | OffscreenCanvas): Promise<Blob> {
@@ -38,7 +98,8 @@ async function canvasToBlob(canvas: HTMLCanvasElement | OffscreenCanvas): Promis
38
98
  }
39
99
 
40
100
  interface DecodedResources {
41
- urls: string[];
101
+ urls: (string | undefined)[];
102
+ held: HeldFrame[];
42
103
  canvases: Set<HTMLCanvasElement | OffscreenCanvas>;
43
104
  }
44
105
 
@@ -48,12 +109,18 @@ interface ThumbnailCanvasSink {
48
109
  ): AsyncIterable<{ canvas: HTMLCanvasElement | OffscreenCanvas } | null>;
49
110
  }
50
111
 
112
+ interface SourceInfo {
113
+ aspect: number;
114
+ metadataDuration: number | null;
115
+ }
116
+
51
117
  function throwIfAborted(signal: AbortSignal): void {
52
118
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
53
119
  }
54
120
 
55
121
  function releaseDecodedResources(resources: DecodedResources): void {
56
- for (const url of resources.urls.splice(0)) URL.revokeObjectURL(url);
122
+ resources.urls.length = 0;
123
+ releaseSharedFrames(resources.held);
57
124
  for (const canvas of resources.canvases) {
58
125
  canvas.width = 0;
59
126
  canvas.height = 0;
@@ -75,19 +142,71 @@ function targetDimensions(
75
142
  };
76
143
  }
77
144
 
145
+ function stripTimes(
146
+ request: VideoThumbnailDecodeRequest,
147
+ info: SourceInfo,
148
+ budgets: Readonly<TimelineViewportBudgets>,
149
+ ) {
150
+ const requestedStart = Math.max(0, request.sourceStart ?? 0);
151
+ const sourceDuration = Math.max(
152
+ 0,
153
+ info.metadataDuration ?? requestedStart + (request.sourceRangeDuration ?? 0),
154
+ );
155
+ const sourceStart = Math.min(requestedStart, sourceDuration);
156
+ const requestedDuration =
157
+ request.sourceRangeDuration ?? Math.max(0, sourceDuration - sourceStart);
158
+ const duration = Math.min(
159
+ Math.max(0, requestedDuration),
160
+ Math.max(0, sourceDuration - sourceStart),
161
+ );
162
+ const timestamps = videoThumbnailTimestamps(
163
+ sourceStart,
164
+ duration,
165
+ Math.min(request.frameCount, budgets.richPreviewFrameCount),
166
+ );
167
+ const slices = Math.max(1, timestamps.length - 1);
168
+ const window: FrameWindow = {
169
+ sourceStart,
170
+ lead: duration / Math.max(2, slices) / 2,
171
+ end: timestamps.length > 1 ? timestamps.at(-1)! : Infinity,
172
+ };
173
+ return { window, timestamps };
174
+ }
175
+
176
+ function takeDecodedFrames(
177
+ { timestamps, window }: ReturnType<typeof stripTimes>,
178
+ keyOf: (time: number) => string,
179
+ resources: DecodedResources,
180
+ ): number[] {
181
+ const missing: number[] = [];
182
+ timestamps.forEach((time, slot) => {
183
+ const key = keyOf(time);
184
+ const frame = takeSharedFrame(key, time, window);
185
+ if (!frame) return void missing.push(slot);
186
+ resources.urls[slot] = frame.url;
187
+ resources.held.push({ key, frame });
188
+ });
189
+ return missing;
190
+ }
191
+
78
192
  async function decodeFrames(
79
193
  sink: ThumbnailCanvasSink,
80
- timestamps: AsyncIterable<number>,
194
+ times: AsyncIterable<number>,
195
+ slots: { slot: number; key: string; decodedAt?: number }[],
81
196
  signal: AbortSignal,
82
197
  resources: DecodedResources,
83
198
  ): Promise<void> {
84
- for await (const wrapped of sink.canvasesAtTimestamps(timestamps)) {
199
+ let next = 0;
200
+ for await (const wrapped of sink.canvasesAtTimestamps(times)) {
85
201
  throwIfAborted(signal);
86
- if (!wrapped) continue;
202
+ const target = slots[next++];
203
+ if (!wrapped || target?.decodedAt === undefined) continue;
87
204
  resources.canvases.add(wrapped.canvas);
88
205
  const blob = await canvasToBlob(wrapped.canvas);
89
206
  throwIfAborted(signal);
90
- resources.urls.push(URL.createObjectURL(blob));
207
+ const frame = shareFrame(target.key, target.decodedAt, URL.createObjectURL(blob));
208
+ resources.urls[target.slot] = frame.url;
209
+ resources.held.push({ key: target.key, frame });
91
210
  }
92
211
  throwIfAborted(signal);
93
212
  }
@@ -95,18 +214,25 @@ async function decodeFrames(
95
214
  function loadedResult(
96
215
  resources: DecodedResources,
97
216
  aspect: number,
98
- width: number,
99
- height: number,
217
+ budgets: Readonly<TimelineViewportBudgets>,
218
+ frameCount = resources.urls.length,
100
219
  ): ThumbnailLoadedResult {
101
- const firstUrl = resources.urls[0];
220
+ const decoded = resources.urls.filter((url): url is string => url !== undefined);
221
+ const firstUrl = decoded[0];
102
222
  if (!firstUrl) throw new Error("Video source returned no thumbnail frames");
223
+ let nearest = firstUrl;
224
+ const urls = Array.from(
225
+ { length: frameCount },
226
+ (_, slot) => (nearest = resources.urls[slot] ?? nearest),
227
+ );
228
+ const { width, height } = targetDimensions(aspect, budgets);
103
229
  const value: ThumbnailValue =
104
- resources.urls.length === 1
230
+ urls.length === 1
105
231
  ? { kind: "image", url: firstUrl, aspect }
106
- : { kind: "filmstrip", urls: [...resources.urls], aspect };
232
+ : { kind: "filmstrip", urls, aspect };
107
233
  return {
108
234
  value,
109
- weight: width * height * 4 * resources.urls.length,
235
+ weight: width * height * 4 * decoded.length,
110
236
  dispose: () => releaseDecodedResources(resources),
111
237
  };
112
238
  }
@@ -116,6 +242,34 @@ export async function decodeVideoThumbnail(
116
242
  request: VideoThumbnailDecodeRequest,
117
243
  signal: AbortSignal,
118
244
  budgets: Readonly<TimelineViewportBudgets> = TIMELINE_VIEWPORT_BUDGETS,
245
+ ): Promise<ThumbnailLoadedResult> {
246
+ const fit = request.fit ?? "cover";
247
+ const sourceKey = createThumbnailKey({ source: request.source, version: request.contentVersion });
248
+ const keyOf = (time: number) => `${sourceKey}\u0000${fit}\u0000${time}`;
249
+ const resources: DecodedResources = { urls: [], held: [], canvases: new Set() };
250
+ const entry = sourceInfos.get(request.source);
251
+ const known = entry?.version === request.contentVersion ? entry?.info : undefined;
252
+ try {
253
+ if (known) {
254
+ if (takeDecodedFrames(stripTimes(request, known, budgets), keyOf, resources).length === 0) {
255
+ throwIfAborted(signal);
256
+ return loadedResult(resources, known.aspect, budgets);
257
+ }
258
+ releaseDecodedResources(resources);
259
+ }
260
+ return await decodeMissingFrames(request, signal, budgets, keyOf, resources);
261
+ } catch (error) {
262
+ releaseDecodedResources(resources);
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ async function decodeMissingFrames(
268
+ request: VideoThumbnailDecodeRequest,
269
+ signal: AbortSignal,
270
+ budgets: Readonly<TimelineViewportBudgets>,
271
+ keyOf: (time: number) => string,
272
+ resources: DecodedResources,
119
273
  ): Promise<ThumbnailLoadedResult> {
120
274
  const mediabunny = await import("mediabunny");
121
275
  throwIfAborted(signal);
@@ -124,7 +278,6 @@ export async function decodeVideoThumbnail(
124
278
  source: new mediabunny.UrlSource(request.source),
125
279
  formats: mediabunny.ALL_FORMATS,
126
280
  });
127
- const resources: DecodedResources = { urls: [], canvases: new Set() };
128
281
  try {
129
282
  const track = await input.getPrimaryVideoTrack();
130
283
  throwIfAborted(signal);
@@ -139,46 +292,36 @@ export async function decodeVideoThumbnail(
139
292
  }
140
293
  const metadataDuration = await track.getDurationFromMetadata({ skipLiveWait: true });
141
294
  throwIfAborted(signal);
142
- const requestedStart = Math.max(0, request.sourceStart ?? 0);
143
- const sourceDuration = Math.max(
144
- 0,
145
- metadataDuration ?? requestedStart + (request.sourceRangeDuration ?? 0),
146
- );
147
- const sourceStart = Math.min(requestedStart, sourceDuration);
148
- const requestedDuration =
149
- request.sourceRangeDuration ?? Math.max(0, sourceDuration - sourceStart);
150
- const duration = Math.min(
151
- Math.max(0, requestedDuration),
152
- Math.max(0, sourceDuration - sourceStart),
153
- );
154
- const timestamps = videoThumbnailTimestamps(
155
- sourceStart,
156
- duration,
157
- Math.min(request.frameCount, budgets.richPreviewFrameCount),
158
- );
159
- const keys = new mediabunny.EncodedPacketSink(track);
160
- const maxKeyframeLead = duration / Math.max(2, timestamps.length - 1) / 2;
161
- async function* decodeTimesAtNearbyKeyframes() {
162
- for (const time of timestamps) {
163
- const key = await keys.getKeyPacket(time, { metadataOnly: true });
164
- if (signal.aborted) return;
165
- const near = key && key.timestamp >= sourceStart && time - key.timestamp <= maxKeyframeLead;
166
- yield near ? key.timestamp : time;
295
+ const info: SourceInfo = { aspect: displayWidth / displayHeight, metadataDuration };
296
+ sourceInfos.set(request.source, { version: request.contentVersion, info });
297
+ const strip = stripTimes(request, info, budgets);
298
+ const { timestamps, window } = strip;
299
+ const slots: { slot: number; key: string; decodedAt?: number }[] = takeDecodedFrames(
300
+ strip,
301
+ keyOf,
302
+ resources,
303
+ ).map((slot) => ({ slot, key: keyOf(timestamps[slot]!) }));
304
+ if (slots.length > 0) {
305
+ const keys = new mediabunny.EncodedPacketSink(track);
306
+ async function* decodeTimesAtNearbyKeyframes() {
307
+ for (const target of slots) {
308
+ const time = timestamps[target.slot]!;
309
+ const key = await keys.getKeyPacket(time, { metadataOnly: true });
310
+ if (signal.aborted) return;
311
+ target.decodedAt = key && accepts(window, time, key.timestamp) ? key.timestamp : time;
312
+ yield target.decodedAt;
313
+ }
167
314
  }
315
+ const target = targetDimensions(info.aspect, budgets);
316
+ const sink = new mediabunny.CanvasSink(track, {
317
+ width: target.width,
318
+ height: target.height,
319
+ fit: request.fit ?? "cover",
320
+ poolSize: 1,
321
+ });
322
+ await decodeFrames(sink, decodeTimesAtNearbyKeyframes(), slots, signal, resources);
168
323
  }
169
- const aspect = displayWidth / displayHeight;
170
- const target = targetDimensions(aspect, budgets);
171
- const sink = new mediabunny.CanvasSink(track, {
172
- width: target.width,
173
- height: target.height,
174
- fit: request.fit ?? "cover",
175
- poolSize: 1,
176
- });
177
- await decodeFrames(sink, decodeTimesAtNearbyKeyframes(), signal, resources);
178
- return loadedResult(resources, aspect, target.width, target.height);
179
- } catch (error) {
180
- releaseDecodedResources(resources);
181
- throw error;
324
+ return loadedResult(resources, info.aspect, budgets, timestamps.length);
182
325
  } finally {
183
326
  input.dispose();
184
327
  }
@@ -9,7 +9,7 @@ describe("timeline viewport budgets", () => {
9
9
  expect(TIMELINE_VIEWPORT_BUDGETS).toMatchObject({
10
10
  directScrollSafetyPx: 8_000_000,
11
11
  rowOverscanPerSide: 2,
12
- timeOverscanViewportRatio: 0.25,
12
+ timeOverscanViewportRatio: 0.5,
13
13
  maxMountedRows: 64,
14
14
  maxMountedClipRoots: 512,
15
15
  maxMountedClipRootsPerRow: 128,
@@ -61,7 +61,7 @@ export const MAX_VISIBLE_THUMBNAIL_FRAMES = Math.ceil(3840 / (66 * (16 / 9))); /
61
61
  export const TIMELINE_VIEWPORT_BUDGETS: Readonly<TimelineViewportBudgets> = Object.freeze({
62
62
  directScrollSafetyPx: 8_000_000,
63
63
  rowOverscanPerSide: 2,
64
- timeOverscanViewportRatio: 0.25,
64
+ timeOverscanViewportRatio: 0.5,
65
65
  maxMountedRows: 64,
66
66
  maxMountedClipRoots: 512,
67
67
  maxMountedClipRootsPerRow: 128,
@@ -1,16 +1,14 @@
1
1
  // Real Chrome: the box GSAP and a stylesheet translate produce together is what no DOM emulation computes.
2
2
  import { mkdtempSync, readFileSync } from "node:fs";
3
- import { createRequire } from "node:module";
4
3
  import { tmpdir } from "node:os";
5
4
  import { join } from "node:path";
6
5
  import { fileURLToPath } from "node:url";
7
- import puppeteer, { type Browser } from "puppeteer-core";
6
+ import type { Browser } from "puppeteer-core";
8
7
  import { build, type Plugin } from "esbuild";
9
8
  import { afterAll, beforeAll, expect, it, vi } from "vitest";
10
- import { findSystemChrome } from "../../vite.browser";
9
+ import { launchTestChrome, showWithRuntime } from "../../tests/chromeTestUtils";
11
10
  import { writeFixture } from "../../tests/e2e/edit-accuracy/grid.mjs";
12
11
 
13
- const require = createRequire(import.meta.url);
14
12
  const CROP = "inset(0px 40px 0px 0px)";
15
13
  // Real Chrome on a loaded Windows runner exceeds Vitest's 5s test and 10s hook defaults.
16
14
  vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
@@ -44,9 +42,7 @@ async function bundle(file: string, name: string): Promise<string> {
44
42
  }
45
43
 
46
44
  beforeAll(async () => {
47
- const executablePath = findSystemChrome();
48
- if (!executablePath) throw new Error("no Chrome found: set HYPERFRAMES_BROWSER_PATH");
49
- browser = await puppeteer.launch({ executablePath, headless: true, args: ["--no-sandbox"] });
45
+ browser = await launchTestChrome();
50
46
  undoBundle = await bundle("./gsapUndoRestore.ts", "hfUndo");
51
47
  softReloadBundle = await bundle("./gsapSoftReload.ts", "hfSoftReload");
52
48
  });
@@ -62,16 +58,7 @@ async function openCase(spec: Spec, moduleBundle: string) {
62
58
  const restored = readFileSync(join(dir, "index.html"), "utf8");
63
59
  const page = await browser.newPage();
64
60
  await page.setViewport({ width: 1920, height: 1080 });
65
- await page.setRequestInterception(true);
66
- // Only the fixture's GSAP CDN script is served; anything else is refused, never fetched.
67
- page.on("request", (request) =>
68
- request.url().endsWith("/gsap.min.js")
69
- ? request.respond({ body: readFileSync(require.resolve("gsap/dist/gsap.min.js"), "utf8") })
70
- : request.abort("blockedbyclient"),
71
- );
72
- await page.setContent(restored, { waitUntil: "load" });
73
- await page.evaluate(readFileSync(require.resolve("@hyperframes/core/runtime"), "utf8"));
74
- await page.waitForFunction(() => "__player" in window);
61
+ await showWithRuntime(page, restored);
75
62
  await page.evaluate(moduleBundle);
76
63
  await page.evaluate(
77
64
  (time) => (window as unknown as { __player: { seek(t: number): void } }).__player.seek(time),