@hyperframes/studio 0.8.139 → 0.8.141

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 (76) hide show
  1. package/dist/assets/hyperframes-player-5Vx2AiUz.js +473 -0
  2. package/dist/assets/{index-DsEkE5_G.js → index-BAnh7cSQ.js} +1 -1
  3. package/dist/assets/{index-Br1Whc0f.js → index-C0thd4eM.js} +204 -204
  4. package/dist/assets/{index-BmIPrb8I.js → index-P8fgj_Xa.js} +1 -1
  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 +6518 -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 +60 -1
  26. package/src/components/nle/NLEPreview.tsx +9 -2
  27. package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +43 -0
  28. package/src/components/sidebar/CompositionsTab.tsx +17 -0
  29. package/src/hooks/externalFileChangeOwnership.test.ts +25 -4
  30. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +3 -22
  31. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +536 -10
  32. package/src/hooks/useExternalFileChangeCoordinator.ts +72 -45
  33. package/src/hooks/useMusicBeatAnalysis.ts +2 -1
  34. package/src/hooks/useSdkSession.lifecycle.test.tsx +20 -0
  35. package/src/hooks/useSdkSession.ts +4 -1
  36. package/src/hooks/useThumbnailStripSize.test.tsx +65 -0
  37. package/src/hooks/useThumbnailStripSize.ts +23 -1
  38. package/src/index.ts +1 -0
  39. package/src/player/components/AudioWaveform.test.tsx +153 -1
  40. package/src/player/components/AudioWaveform.tsx +93 -37
  41. package/src/player/components/Timeline.scrollRest.test.tsx +22 -0
  42. package/src/player/components/Timeline.test.ts +3 -1
  43. package/src/player/components/TimelineCanvas.tsx +4 -0
  44. package/src/player/components/TimelineClip.tsx +20 -6
  45. package/src/player/components/TimelineClipFades.test.tsx +11 -0
  46. package/src/player/components/TimelineClipFades.tsx +4 -4
  47. package/src/player/components/TimelineClipJoins.tsx +23 -18
  48. package/src/player/components/TimelineGestureOverlay.tsx +30 -27
  49. package/src/player/components/TimelineGroupRow.tsx +1 -0
  50. package/src/player/components/TimelineLaneClip.tsx +127 -0
  51. package/src/player/components/TimelineLanes.test.tsx +51 -6
  52. package/src/player/components/TimelineLanes.tsx +166 -178
  53. package/src/player/components/TimelineRuler.tsx +2 -0
  54. package/src/player/components/TimelineTimeLayer.tsx +25 -0
  55. package/src/player/components/VideoThumbnail.test.tsx +204 -12
  56. package/src/player/components/VideoThumbnail.tsx +37 -9
  57. package/src/player/components/thumbnailUtils.test.ts +17 -3
  58. package/src/player/components/thumbnailUtils.ts +17 -1
  59. package/src/player/components/timelineLayout.test.ts +3 -3
  60. package/src/player/components/timelineMotion.test.ts +27 -0
  61. package/src/player/components/timelineMotion.ts +62 -0
  62. package/src/player/components/timelineZoomInput.test.ts +438 -0
  63. package/src/player/components/timelineZoomInput.ts +456 -0
  64. package/src/player/components/useTimelineGeometry.ts +0 -3
  65. package/src/player/components/useTimelinePlayhead.test.tsx +97 -30
  66. package/src/player/components/useTimelinePlayhead.ts +53 -59
  67. package/src/player/components/useTimelineProviderState.tsx +16 -21
  68. package/src/player/components/useTimelineZoom.ts +7 -12
  69. package/src/player/lib/runtimeProtocol.ts +6 -0
  70. package/src/player/lib/thumbnailVideoDecoder.test.ts +230 -30
  71. package/src/player/lib/thumbnailVideoDecoder.ts +197 -54
  72. package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
  73. package/src/player/lib/timelineViewportBudgets.ts +1 -1
  74. package/src/utils/gsapUndoRestore.browser.test.ts +4 -17
  75. package/dist/assets/hyperframes-player-CckOoCJ8.js +0 -473
  76. /package/dist/{chunk-E422JDF5.js.map → chunk-H2DUOXTF.js.map} +0 -0
@@ -39,10 +39,20 @@ function recordDecodes(decoded: number[][]): void {
39
39
  });
40
40
  }
41
41
 
42
+ const endOf = (start: number, duration: number) => start + Math.max(0, duration - 0.001);
43
+
44
+ // Decoded frames are shared per source for the module's life, so each test decodes its own file.
45
+ let sources = 0;
46
+ let source = "";
47
+
42
48
  beforeEach(() => {
49
+ source = `/clip-${++sources}.mp4`;
43
50
  vi.clearAllMocks();
44
51
  getKeyPacket.mockImplementation(async () => null);
45
- vi.spyOn(URL, "createObjectURL").mockReturnValueOnce("blob:one").mockReturnValueOnce("blob:two");
52
+ vi.spyOn(URL, "createObjectURL")
53
+ .mockReturnValueOnce("blob:one")
54
+ .mockReturnValueOnce("blob:two")
55
+ .mockReturnValueOnce("blob:end");
46
56
  vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => {});
47
57
  HTMLCanvasElement.prototype.toBlob = function toBlob(callback) {
48
58
  callback(new Blob(["frame"], { type: "image/jpeg" }));
@@ -55,9 +65,15 @@ beforeEach(() => {
55
65
  });
56
66
 
57
67
  describe("videoThumbnailTimestamps", () => {
58
- it("uses the midpoint for a poster and sorted sparse points for a strip", () => {
68
+ it("uses the midpoint for a poster, and each slice's left edge then the end for a strip", () => {
59
69
  expect(videoThumbnailTimestamps(2, 6, 1)).toEqual([5]);
60
- expect(videoThumbnailTimestamps(2, 6, 4)).toEqual([2, 4, 6, 8]);
70
+ expect(videoThumbnailTimestamps(2, 6, 4)).toEqual([2, 3.5, 5, 6.5, endOf(2, 6)]);
71
+ });
72
+
73
+ it("nests each strip in the strip twice as long, and the poster in every strip", () => {
74
+ const strip = videoThumbnailTimestamps(0, 8, 4);
75
+ expect(videoThumbnailTimestamps(0, 8, 8).filter((_, i) => i % 2 === 0)).toEqual(strip);
76
+ expect(strip).toContain(videoThumbnailTimestamps(0, 8, 1)[0]);
61
77
  });
62
78
 
63
79
  it("clamps invalid source ranges", () => {
@@ -71,31 +87,31 @@ describe("decodeVideoThumbnail", () => {
71
87
  const decoded: number[][] = [];
72
88
  recordDecodes(decoded);
73
89
  const result = await decodeVideoThumbnail(
74
- { source: "/clip.mp4", sourceStart: 2, sourceRangeDuration: 6, frameCount: 2 },
90
+ { source, sourceStart: 2, sourceRangeDuration: 6, frameCount: 2 },
75
91
  new AbortController().signal,
76
92
  );
77
93
 
78
- expect(decoded).toEqual([[2, 8]]);
94
+ expect(decoded).toEqual([[2, 5, endOf(2, 6)]]);
79
95
  expect(result.value).toEqual({
80
96
  kind: "filmstrip",
81
- urls: ["blob:one", "blob:two"],
97
+ urls: ["blob:one", "blob:two", "blob:end"],
82
98
  aspect: 9 / 16,
83
99
  });
84
100
  result.dispose?.();
85
101
  result.dispose?.();
86
- expect(URL.revokeObjectURL).toHaveBeenCalledTimes(2);
102
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(3);
87
103
  expect(dispose).toHaveBeenCalledTimes(1);
88
104
  });
89
105
 
90
106
  it("decodes each strip frame at its keyframe unless that keyframe is before the clip's range", async () => {
91
- getKeyPacket.mockImplementation(async (time) => ({ timestamp: Math.floor(time / 4) * 4 }));
107
+ getKeyPacket.mockImplementation(async (time) => ({ timestamp: time - 0.5 }));
92
108
  const decoded: number[][] = [];
93
109
  recordDecodes(decoded);
94
110
  await decodeVideoThumbnail(
95
- { source: "/clip.mp4", sourceStart: 2, sourceRangeDuration: 8, frameCount: 3 },
111
+ { source, sourceStart: 2, sourceRangeDuration: 8, frameCount: 4 },
96
112
  new AbortController().signal,
97
113
  );
98
- expect(decoded).toEqual([[2, 4, 8]]);
114
+ expect(decoded).toEqual([[2, 3.5, 5.5, 7.5, endOf(2, 8)]]);
99
115
  });
100
116
 
101
117
  it("keeps a slot's own time when its keyframe is more than half a slot earlier", async () => {
@@ -103,14 +119,10 @@ describe("decodeVideoThumbnail", () => {
103
119
  const decoded: number[][] = [];
104
120
  recordDecodes(decoded);
105
121
  await decodeVideoThumbnail(
106
- { source: "/clip.mp4", sourceStart: 0, sourceRangeDuration: 10, frameCount: 3 },
107
- new AbortController().signal,
108
- );
109
- await decodeVideoThumbnail(
110
- { source: "/clip.mp4", sourceStart: 0, sourceRangeDuration: 10, frameCount: 1 },
122
+ { source, sourceStart: 0, sourceRangeDuration: 10, frameCount: 4 },
111
123
  new AbortController().signal,
112
124
  );
113
- expect(decoded).toEqual([[0, 5, 10], [5]]);
125
+ expect(decoded).toEqual([[0, 2.5, 5, 7.5, endOf(0, 10)]]);
114
126
  });
115
127
 
116
128
  it("looks up each keyframe just before decoding it", async () => {
@@ -126,10 +138,11 @@ describe("decodeVideoThumbnail", () => {
126
138
  }
127
139
  });
128
140
  await decodeVideoThumbnail(
129
- { source: "/clip.mp4", sourceStart: 0, sourceRangeDuration: 10, frameCount: 3 },
141
+ { source, sourceStart: 0, sourceRangeDuration: 10, frameCount: 2 },
130
142
  new AbortController().signal,
131
143
  );
132
- expect(events).toEqual(["key 0", "frame 0", "key 5", "frame 5", "key 10", "frame 10"]);
144
+ const end = endOf(0, 10);
145
+ expect(events).toEqual(["key 0", "frame 0", "key 5", "frame 5", `key ${end}`, `frame ${end}`]);
133
146
  });
134
147
 
135
148
  it("ends the decode times without throwing when cancelled during a keyframe lookup", async () => {
@@ -146,7 +159,7 @@ describe("decodeVideoThumbnail", () => {
146
159
  });
147
160
  await expect(
148
161
  decodeVideoThumbnail(
149
- { source: "/clip.mp4", sourceStart: 0, sourceRangeDuration: 10, frameCount: 3 },
162
+ { source, sourceStart: 0, sourceRangeDuration: 10, frameCount: 3 },
150
163
  controller.signal,
151
164
  ),
152
165
  ).rejects.toThrow("Aborted");
@@ -164,10 +177,198 @@ describe("decodeVideoThumbnail", () => {
164
177
  const decoded: number[][] = [];
165
178
  recordDecodes(decoded);
166
179
  await decodeVideoThumbnail(
167
- { source: "/clip.mp4", sourceStart: 5, sourceRangeDuration: 5, frameCount: 3 },
180
+ { source, sourceStart: 5, sourceRangeDuration: 5, frameCount: 2 },
168
181
  new AbortController().signal,
169
182
  );
170
- expect(decoded).toEqual([[5, 7.5, 10]]);
183
+ expect(decoded).toEqual([[5, 7.5, endOf(5, 5)]]);
184
+ });
185
+
186
+ it("reuses the frames a strip of the same source shows, so a zoom decodes only new times", async () => {
187
+ vi.mocked(URL.createObjectURL).mockImplementation(() => `blob:${Math.random()}`);
188
+ const decoded: number[][] = [];
189
+ recordDecodes(decoded);
190
+ const range = { source, sourceStart: 0, sourceRangeDuration: 8 };
191
+ const signal = new AbortController().signal;
192
+ const four = await decodeVideoThumbnail({ ...range, frameCount: 4 }, signal);
193
+ // Twice the frames decodes only the new half; half the frames decodes nothing. All share the end.
194
+ const eight = await decodeVideoThumbnail({ ...range, frameCount: 8 }, signal);
195
+ const two = await decodeVideoThumbnail({ ...range, frameCount: 2 }, signal);
196
+ expect(decoded).toEqual([
197
+ [0, 2, 4, 6, endOf(0, 8)],
198
+ [1, 3, 5, 7],
199
+ ]);
200
+ const urls = (result: typeof four) =>
201
+ result.value.kind === "filmstrip" ? result.value.urls : [];
202
+ expect(urls(eight).filter((_, i) => i % 2 === 0)).toEqual(urls(four));
203
+ expect(urls(two)).toEqual([urls(four)[0], urls(four)[2], urls(four)[4]]);
204
+ for (const result of [four, eight, two]) result.dispose?.();
205
+ });
206
+
207
+ it("decodes a frame again when the one shared for its time sits too far before it", async () => {
208
+ // Every keyframe is 2 s before the time asked: a 2-frame strip of 8 s accepts that (its
209
+ // frames stand for 4 s each), a 4-frame strip does not, or its frames would run backwards.
210
+ getKeyPacket.mockImplementation(async (time) => ({ timestamp: time - 2 }));
211
+ const decoded: number[][] = [];
212
+ recordDecodes(decoded);
213
+ const range = { source, sourceStart: 0, sourceRangeDuration: 8 };
214
+ const signal = new AbortController().signal;
215
+ const two = await decodeVideoThumbnail({ ...range, frameCount: 2 }, signal);
216
+ const four = await decodeVideoThumbnail({ ...range, frameCount: 4 }, signal);
217
+ expect(decoded).toEqual([
218
+ [0, 2, endOf(0, 8)],
219
+ [2, 4, 6],
220
+ ]);
221
+ two.dispose?.();
222
+ four.dispose?.();
223
+ });
224
+
225
+ it("returns a strip whose frames are all shared without opening the file again", async () => {
226
+ recordDecodes([]);
227
+ const range = { source, sourceStart: 0, sourceRangeDuration: 8 };
228
+ const signal = new AbortController().signal;
229
+ const four = await decodeVideoThumbnail({ ...range, frameCount: 4 }, signal);
230
+ const two = await decodeVideoThumbnail({ ...range, frameCount: 2 }, signal);
231
+ expect(input.getPrimaryVideoTrack).toHaveBeenCalledTimes(1);
232
+ four.dispose?.();
233
+ two.dispose?.();
234
+ });
235
+
236
+ it("reopens the same asset after a project round trip and keeps new-session frames separate", async () => {
237
+ let frame = 0;
238
+ vi.mocked(URL.createObjectURL)
239
+ .mockReset()
240
+ .mockImplementation(() => `blob:version-${++frame}`);
241
+ const decoded: number[][] = [];
242
+ recordDecodes(decoded);
243
+ const signal = new AbortController().signal;
244
+ const original = await decodeVideoThumbnail(
245
+ { source, contentVersion: "A:1", frameCount: 1 },
246
+ signal,
247
+ );
248
+ const other = await decodeVideoThumbnail(
249
+ { source: `${source}?B`, contentVersion: "B:2", frameCount: 1 },
250
+ signal,
251
+ );
252
+ input.getPrimaryVideoTrack.mockResolvedValue({
253
+ getDisplayWidth: vi.fn(async () => 1280),
254
+ getDisplayHeight: vi.fn(async () => 360),
255
+ getDurationFromMetadata: vi.fn(async () => 10),
256
+ });
257
+ const reopened = await decodeVideoThumbnail(
258
+ { source, contentVersion: "A:3", frameCount: 1 },
259
+ signal,
260
+ );
261
+ const shared = await decodeVideoThumbnail(
262
+ { source, contentVersion: "A:3", frameCount: 1 },
263
+ signal,
264
+ );
265
+ expect(reopened.value).toEqual({ kind: "image", url: "blob:version-3", aspect: 32 / 9 });
266
+ expect(shared.value).toEqual(reopened.value);
267
+ expect(decoded).toEqual([[5], [5], [5]]);
268
+ expect(input.getPrimaryVideoTrack).toHaveBeenCalledTimes(3);
269
+ original.dispose?.();
270
+ expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:version-1");
271
+ expect(URL.revokeObjectURL).not.toHaveBeenCalledWith("blob:version-3");
272
+ other.dispose?.();
273
+ reopened.dispose?.();
274
+ expect(URL.revokeObjectURL).not.toHaveBeenCalledWith("blob:version-3");
275
+ shared.dispose?.();
276
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(3);
277
+ });
278
+
279
+ it("keeps one copy of a frame two strips decode at once, and revokes the other", async () => {
280
+ vi.mocked(URL.createObjectURL).mockImplementation(() => `blob:${Math.random()}`);
281
+ // The first strip holds its first frame until the second is decoding too.
282
+ let release!: () => void;
283
+ const bothDecoding = new Promise<void>((resolve) => (release = resolve));
284
+ canvasesAtTimestamps.mockImplementation(async function* (timestamps: AsyncIterable<number>) {
285
+ const first = canvasesAtTimestamps.mock.calls.length === 1;
286
+ for await (const _time of timestamps) {
287
+ if (first) await bothDecoding;
288
+ else release();
289
+ yield { canvas: document.createElement("canvas") };
290
+ }
291
+ });
292
+ const range = { source, sourceStart: 0, sourceRangeDuration: 8 };
293
+ const signal = new AbortController().signal;
294
+ const decodingTwo = decodeVideoThumbnail({ ...range, frameCount: 2 }, signal);
295
+ await vi.waitFor(() => expect(canvasesAtTimestamps).toHaveBeenCalledOnce());
296
+ const [two, four] = await Promise.all([
297
+ decodingTwo,
298
+ decodeVideoThumbnail({ ...range, frameCount: 4 }, signal),
299
+ ]);
300
+ // Both decoded the frames at 0 s, 4 s and the end; the second copy of each goes at once.
301
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(3);
302
+ two.dispose?.();
303
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(3);
304
+ four.dispose?.();
305
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(8);
306
+ });
307
+
308
+ it("fills a frame that failed to decode from the one before it, so tiles stay on their times", async () => {
309
+ canvasesAtTimestamps.mockImplementation(async function* (timestamps: AsyncIterable<number>) {
310
+ let slot = 0;
311
+ for await (const _time of timestamps) {
312
+ yield slot++ === 2 ? null : { canvas: document.createElement("canvas") };
313
+ }
314
+ });
315
+ const signal = new AbortController().signal;
316
+ const result = await decodeVideoThumbnail(
317
+ { source, sourceStart: 0, sourceRangeDuration: 8, frameCount: 2 },
318
+ signal,
319
+ );
320
+ expect(result.value).toMatchObject({
321
+ kind: "filmstrip",
322
+ urls: ["blob:one", "blob:two", "blob:two"],
323
+ });
324
+ // The cache is charged for the two decoded frames, not the filled copy.
325
+ recordDecodes([]);
326
+ const whole = await decodeVideoThumbnail(
327
+ { source: `${source}?whole`, sourceStart: 0, sourceRangeDuration: 8, frameCount: 2 },
328
+ signal,
329
+ );
330
+ expect(result.weight * 3).toBe(whole.weight * 2);
331
+ result.dispose?.();
332
+ whole.dispose?.();
333
+ });
334
+
335
+ it("snaps a poster up to a quarter of its range and a strip frame up to half a slice", async () => {
336
+ const decoded: number[][] = [];
337
+ recordDecodes(decoded);
338
+ const signal = new AbortController().signal;
339
+ getKeyPacket.mockImplementation(async (time) => ({ timestamp: time - 2.4 }));
340
+ await decodeVideoThumbnail(
341
+ { source, sourceStart: 0, sourceRangeDuration: 10, frameCount: 1 },
342
+ signal,
343
+ );
344
+ getKeyPacket.mockImplementation(async (time) => ({ timestamp: time - 2.6 }));
345
+ await decodeVideoThumbnail(
346
+ { source: `${source}?b`, sourceStart: 0, sourceRangeDuration: 10, frameCount: 1 },
347
+ signal,
348
+ );
349
+ // Four slices of 8 s: a keyframe 0.9 s early snaps, one 1.1 s early does not.
350
+ getKeyPacket.mockImplementation(async (time) => ({
351
+ timestamp: time - (time === 4 ? 0.9 : 1.1),
352
+ }));
353
+ await decodeVideoThumbnail(
354
+ { source: `${source}?c`, sourceStart: 0, sourceRangeDuration: 8, frameCount: 4 },
355
+ signal,
356
+ );
357
+ expect(decoded).toEqual([[5 - 2.4], [5], [0, 2, 4 - 0.9, 6, endOf(0, 8)]]);
358
+ });
359
+
360
+ it("revokes a shared frame only once no strip shows it", async () => {
361
+ vi.mocked(URL.createObjectURL).mockImplementation(() => `blob:${Math.random()}`);
362
+ recordDecodes([]);
363
+ const range = { source, sourceStart: 0, sourceRangeDuration: 8 };
364
+ const signal = new AbortController().signal;
365
+ const four = await decodeVideoThumbnail({ ...range, frameCount: 4 }, signal);
366
+ const two = await decodeVideoThumbnail({ ...range, frameCount: 2 }, signal);
367
+ four.dispose?.();
368
+ // The three frames the 2-slice strip still shows stay; the other two go.
369
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(2);
370
+ two.dispose?.();
371
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(5);
171
372
  });
172
373
 
173
374
  it("releases input and degrades when the source has no video track", async () => {
@@ -181,13 +382,15 @@ describe("decodeVideoThumbnail", () => {
181
382
  it("revokes partial results when cancellation lands during extraction", async () => {
182
383
  const controller = new AbortController();
183
384
  const canvas = document.createElement("canvas");
184
- canvasesAtTimestamps.mockImplementation(async function* () {
185
- yield { canvas, timestamp: 1, duration: 1 };
186
- controller.abort();
187
- yield { canvas, timestamp: 2, duration: 1 };
385
+ canvasesAtTimestamps.mockImplementation(async function* (timestamps: AsyncIterable<number>) {
386
+ let frames = 0;
387
+ for await (const timestamp of timestamps) {
388
+ if (frames++ === 1) controller.abort();
389
+ yield { canvas, timestamp, duration: 1 };
390
+ }
188
391
  });
189
392
  await expect(
190
- decodeVideoThumbnail({ source: "/clip.mp4", frameCount: 2 }, controller.signal),
393
+ decodeVideoThumbnail({ source, frameCount: 2 }, controller.signal),
191
394
  ).rejects.toMatchObject({ name: "AbortError" });
192
395
  expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1);
193
396
  expect(dispose).toHaveBeenCalledTimes(1);
@@ -207,10 +410,7 @@ describe("decodeVideoThumbnail", () => {
207
410
  getDurationFromMetadata,
208
411
  });
209
412
 
210
- const decoding = decodeVideoThumbnail(
211
- { source: "/clip.mp4", frameCount: 1 },
212
- controller.signal,
213
- );
413
+ const decoding = decodeVideoThumbnail({ source, frameCount: 1 }, controller.signal);
214
414
  await vi.waitFor(() => expect(getDisplayWidth).toHaveBeenCalledOnce());
215
415
  controller.abort();
216
416
  resolveWidth(1080);