@editframe/elements 0.19.2-beta.0 → 0.20.0-beta.0

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 (96) hide show
  1. package/dist/elements/ContextProxiesController.d.ts +40 -0
  2. package/dist/elements/ContextProxiesController.js +69 -0
  3. package/dist/elements/EFCaptions.d.ts +45 -6
  4. package/dist/elements/EFCaptions.js +220 -26
  5. package/dist/elements/EFImage.js +4 -1
  6. package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +2 -1
  7. package/dist/elements/EFMedia/AssetIdMediaEngine.js +9 -0
  8. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -0
  9. package/dist/elements/EFMedia/AssetMediaEngine.js +11 -0
  10. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +13 -1
  11. package/dist/elements/EFMedia/BaseMediaEngine.js +9 -0
  12. package/dist/elements/EFMedia/JitMediaEngine.d.ts +7 -1
  13. package/dist/elements/EFMedia/JitMediaEngine.js +24 -0
  14. package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
  15. package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
  16. package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
  17. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
  18. package/dist/elements/EFMedia.js +25 -1
  19. package/dist/elements/EFSurface.browsertest.d.ts +0 -0
  20. package/dist/elements/EFSurface.d.ts +30 -0
  21. package/dist/elements/EFSurface.js +96 -0
  22. package/dist/elements/EFTemporal.js +7 -6
  23. package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
  24. package/dist/elements/EFThumbnailStrip.d.ts +86 -0
  25. package/dist/elements/EFThumbnailStrip.js +490 -0
  26. package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
  27. package/dist/elements/EFTimegroup.d.ts +7 -7
  28. package/dist/elements/EFTimegroup.js +59 -16
  29. package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
  30. package/dist/elements/updateAnimations.d.ts +5 -0
  31. package/dist/elements/updateAnimations.js +37 -13
  32. package/dist/getRenderInfo.js +1 -1
  33. package/dist/gui/ContextMixin.js +27 -14
  34. package/dist/gui/EFControls.browsertest.d.ts +0 -0
  35. package/dist/gui/EFControls.d.ts +38 -0
  36. package/dist/gui/EFControls.js +51 -0
  37. package/dist/gui/EFFilmstrip.d.ts +40 -1
  38. package/dist/gui/EFFilmstrip.js +240 -3
  39. package/dist/gui/EFPreview.js +2 -1
  40. package/dist/gui/EFScrubber.d.ts +6 -5
  41. package/dist/gui/EFScrubber.js +31 -21
  42. package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
  43. package/dist/gui/EFTimeDisplay.d.ts +2 -6
  44. package/dist/gui/EFTimeDisplay.js +13 -23
  45. package/dist/gui/TWMixin.js +1 -1
  46. package/dist/gui/currentTimeContext.d.ts +3 -0
  47. package/dist/gui/currentTimeContext.js +3 -0
  48. package/dist/gui/durationContext.d.ts +3 -0
  49. package/dist/gui/durationContext.js +3 -0
  50. package/dist/index.d.ts +3 -0
  51. package/dist/index.js +4 -1
  52. package/dist/style.css +1 -1
  53. package/dist/transcoding/types/index.d.ts +11 -0
  54. package/dist/utils/LRUCache.d.ts +46 -0
  55. package/dist/utils/LRUCache.js +382 -1
  56. package/dist/utils/LRUCache.test.d.ts +1 -0
  57. package/package.json +2 -2
  58. package/src/elements/ContextProxiesController.ts +123 -0
  59. package/src/elements/EFCaptions.browsertest.ts +1820 -0
  60. package/src/elements/EFCaptions.ts +373 -36
  61. package/src/elements/EFImage.ts +4 -1
  62. package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
  63. package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
  64. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
  65. package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
  66. package/src/elements/EFMedia/JitMediaEngine.ts +48 -0
  67. package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
  68. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
  69. package/src/elements/EFMedia.ts +38 -1
  70. package/src/elements/EFSurface.browsertest.ts +155 -0
  71. package/src/elements/EFSurface.ts +141 -0
  72. package/src/elements/EFTemporal.ts +14 -8
  73. package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
  74. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
  75. package/src/elements/EFThumbnailStrip.ts +905 -0
  76. package/src/elements/EFTimegroup.browsertest.ts +56 -7
  77. package/src/elements/EFTimegroup.ts +88 -18
  78. package/src/elements/updateAnimations.browsertest.ts +361 -12
  79. package/src/elements/updateAnimations.ts +68 -19
  80. package/src/gui/ContextMixin.browsertest.ts +0 -25
  81. package/src/gui/ContextMixin.ts +44 -20
  82. package/src/gui/EFControls.browsertest.ts +175 -0
  83. package/src/gui/EFControls.ts +84 -0
  84. package/src/gui/EFFilmstrip.ts +323 -4
  85. package/src/gui/EFPreview.ts +2 -1
  86. package/src/gui/EFScrubber.ts +29 -25
  87. package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
  88. package/src/gui/EFTimeDisplay.ts +12 -40
  89. package/src/gui/currentTimeContext.ts +5 -0
  90. package/src/gui/durationContext.ts +3 -0
  91. package/src/transcoding/types/index.ts +13 -0
  92. package/src/utils/LRUCache.test.ts +272 -0
  93. package/src/utils/LRUCache.ts +543 -0
  94. package/types.json +1 -1
  95. package/dist/transcoding/cache/CacheManager.d.ts +0 -73
  96. package/src/transcoding/cache/CacheManager.ts +0 -208
@@ -0,0 +1,713 @@
1
+ import { beforeAll, beforeEach, describe, vi } from "vitest";
2
+ import { test as baseTest } from "../../test/useMSW.js";
3
+ import type { EFTimegroup } from "./EFTimegroup.js";
4
+ import type { EFVideo } from "./EFVideo.js";
5
+ import "./EFVideo.js";
6
+ import "./EFTimegroup.js";
7
+ import "./EFThumbnailStrip.js"; // Import to register the custom element
8
+ import "../gui/EFWorkbench.js";
9
+ import "../gui/EFPreview.js";
10
+ import type { EFConfiguration } from "../gui/EFConfiguration.js";
11
+ import { AssetMediaEngine } from "./EFMedia/AssetMediaEngine.js";
12
+ import { JitMediaEngine } from "./EFMedia/JitMediaEngine.js";
13
+
14
+ beforeAll(async () => {
15
+ console.clear();
16
+ await fetch("/@ef-clear-cache", {
17
+ method: "DELETE",
18
+ });
19
+ });
20
+
21
+ beforeEach(() => {
22
+ localStorage.clear();
23
+ });
24
+
25
+ const test = baseTest.extend<{
26
+ configuration: EFConfiguration;
27
+ timegroup: EFTimegroup;
28
+ jitVideo: EFVideo;
29
+ assetVideo: EFVideo;
30
+ }>({
31
+ configuration: async ({ expect }, use) => {
32
+ const configuration = document.createElement("ef-configuration");
33
+ configuration.innerHTML = `<h1 style="font: 10px monospace">${expect.getState().currentTestName}</h1>`;
34
+ // Use integrated proxy server (same host/port as test runner)
35
+ const apiHost = `${window.location.protocol}//${window.location.host}`;
36
+ configuration.setAttribute("api-host", apiHost);
37
+ configuration.apiHost = apiHost;
38
+ configuration.signingURL = "";
39
+ document.body.appendChild(configuration);
40
+ await use(configuration);
41
+ },
42
+ timegroup: async ({}, use) => {
43
+ const timegroup = document.createElement("ef-timegroup");
44
+ timegroup.setAttribute("mode", "contain");
45
+ await use(timegroup);
46
+ },
47
+ jitVideo: async ({ configuration, timegroup }, use) => {
48
+ const video = document.createElement("ef-video");
49
+ video.src = "http://web:3000/head-moov-480p.mp4";
50
+ timegroup.appendChild(video);
51
+ configuration.appendChild(timegroup);
52
+ await video.mediaEngineTask.run();
53
+ await use(video);
54
+ },
55
+ assetVideo: async ({ configuration, timegroup }, use) => {
56
+ const video = document.createElement("ef-video");
57
+ video.src = "bars-n-tone.mp4";
58
+ timegroup.appendChild(video);
59
+ configuration.appendChild(timegroup);
60
+ await video.mediaEngineTask.run();
61
+ await use(video);
62
+ },
63
+ });
64
+
65
+ describe("MediaEngine Thumbnail Extraction", () => {
66
+ describe("JitMediaEngine", () => {
67
+ test("initializes with JitMediaEngine", async ({ jitVideo, expect }) => {
68
+ const mediaEngine = jitVideo.mediaEngineTask.value;
69
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
70
+ expect(jitVideo.intrinsicDurationMs).toBe(10_000);
71
+ });
72
+
73
+ test("extracts single thumbnail at timestamp", async ({
74
+ jitVideo,
75
+ expect,
76
+ }) => {
77
+ const mediaEngine = jitVideo.mediaEngineTask.value!;
78
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
79
+
80
+ const timestamps = [2000]; // 2 seconds
81
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
82
+
83
+ expect(thumbnails).toHaveLength(1);
84
+ expect(thumbnails[0]).toBeTruthy();
85
+ expect(thumbnails[0]?.timestamp).toBe(2000);
86
+ expect(thumbnails[0]?.thumbnail).toBeDefined();
87
+
88
+ // Verify it's a valid canvas
89
+ const canvas = thumbnails[0]!.thumbnail;
90
+ expect(
91
+ canvas instanceof HTMLCanvasElement ||
92
+ canvas instanceof OffscreenCanvas,
93
+ ).toBe(true);
94
+ expect(canvas.width).toBeGreaterThan(0);
95
+ expect(canvas.height).toBeGreaterThan(0);
96
+ });
97
+
98
+ test("extracts multiple thumbnails in batch", async ({
99
+ jitVideo,
100
+ expect,
101
+ }) => {
102
+ const mediaEngine = jitVideo.mediaEngineTask.value!;
103
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
104
+
105
+ const timestamps = [1000, 3000, 5000, 7000]; // Multiple timestamps
106
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
107
+
108
+ expect(thumbnails).toHaveLength(4);
109
+
110
+ for (let i = 0; i < timestamps.length; i++) {
111
+ const thumbnail = thumbnails[i];
112
+ expect(thumbnail).toBeTruthy();
113
+ expect(thumbnail?.timestamp).toBe(timestamps[i]);
114
+
115
+ const canvas = thumbnail!.thumbnail;
116
+ expect(
117
+ canvas instanceof HTMLCanvasElement ||
118
+ canvas instanceof OffscreenCanvas,
119
+ ).toBe(true);
120
+ expect(canvas.width).toBeGreaterThan(0);
121
+ expect(canvas.height).toBeGreaterThan(0);
122
+ }
123
+ });
124
+
125
+ test("handles timestamps in same segment efficiently", async ({
126
+ jitVideo,
127
+ expect,
128
+ }) => {
129
+ const mediaEngine = jitVideo.mediaEngineTask.value as JitMediaEngine;
130
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
131
+
132
+ // Get segment duration to ensure timestamps are in same segment
133
+ const videoRendition =
134
+ mediaEngine.getScrubVideoRendition() || mediaEngine.getVideoRendition();
135
+ const segmentDurationMs = videoRendition.segmentDurationMs || 2000;
136
+
137
+ // Pick timestamps within the first segment - avoid edge cases near boundaries
138
+ const timestamps = [
139
+ 100,
140
+ 500,
141
+ 1000,
142
+ Math.min(1500, segmentDurationMs - 200),
143
+ ];
144
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
145
+
146
+ expect(thumbnails).toHaveLength(4);
147
+
148
+ // Most should succeed since they're in the same segment
149
+ const successfulThumbnails = thumbnails.filter((t) => t !== null);
150
+ expect(successfulThumbnails.length).toBeGreaterThan(2); // At least 3 out of 4
151
+
152
+ for (const thumbnail of successfulThumbnails) {
153
+ expect(thumbnail!.thumbnail).toBeDefined();
154
+ }
155
+ });
156
+
157
+ test("handles timestamps across different segments", async ({
158
+ jitVideo,
159
+ expect,
160
+ }) => {
161
+ const mediaEngine = jitVideo.mediaEngineTask.value as JitMediaEngine;
162
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
163
+
164
+ // Pick timestamps that span multiple segments
165
+ const timestamps = [500, 2500, 4500, 6500, 8500]; // Across different 2s segments
166
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
167
+
168
+ expect(thumbnails).toHaveLength(5);
169
+
170
+ // All should succeed
171
+ for (let i = 0; i < timestamps.length; i++) {
172
+ const thumbnail = thumbnails[i];
173
+ expect(thumbnail).toBeTruthy();
174
+ expect(thumbnail?.timestamp).toBe(timestamps[i]);
175
+ }
176
+ });
177
+
178
+ test("handles invalid timestamps gracefully", async ({
179
+ jitVideo,
180
+ expect,
181
+ }) => {
182
+ const mediaEngine = jitVideo.mediaEngineTask.value!;
183
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
184
+
185
+ const timestamps = [-1000, 15000]; // Before start and after end
186
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
187
+
188
+ expect(thumbnails).toHaveLength(2);
189
+
190
+ // Invalid timestamps should return null
191
+ expect(thumbnails[0]).toBeNull();
192
+ expect(thumbnails[1]).toBeNull();
193
+ });
194
+
195
+ test("handles mix of valid and invalid timestamps", async ({
196
+ jitVideo,
197
+ expect,
198
+ }) => {
199
+ const mediaEngine = jitVideo.mediaEngineTask.value!;
200
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
201
+
202
+ const timestamps = [-1000, 2000, 15000, 5000]; // Invalid, valid, invalid, valid
203
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
204
+
205
+ expect(thumbnails).toHaveLength(4);
206
+
207
+ expect(thumbnails[0]).toBeNull(); // Invalid
208
+ expect(thumbnails[1]).toBeTruthy(); // Valid
209
+ expect(thumbnails[1]?.timestamp).toBe(2000);
210
+ expect(thumbnails[2]).toBeNull(); // Invalid
211
+ expect(thumbnails[3]).toBeTruthy(); // Valid
212
+ expect(thumbnails[3]?.timestamp).toBe(5000);
213
+ });
214
+
215
+ test("handles empty timestamp array", async ({ jitVideo, expect }) => {
216
+ const mediaEngine = jitVideo.mediaEngineTask.value!;
217
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
218
+
219
+ const timestamps: number[] = [];
220
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
221
+
222
+ expect(thumbnails).toHaveLength(0);
223
+ });
224
+
225
+ test("uses scrub rendition when available", async ({
226
+ jitVideo,
227
+ expect,
228
+ }) => {
229
+ const mediaEngine = jitVideo.mediaEngineTask.value!;
230
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
231
+
232
+ // Check if scrub rendition exists
233
+ const scrubRendition = mediaEngine.getScrubVideoRendition();
234
+ const mainRendition = mediaEngine.getVideoRendition();
235
+
236
+ expect(scrubRendition).toBeDefined();
237
+ expect(mainRendition).toBeDefined();
238
+
239
+ // Extract thumbnail to ensure it works with scrub rendition
240
+ const timestamps = [3000];
241
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
242
+
243
+ expect(thumbnails).toHaveLength(1);
244
+ expect(thumbnails[0]).toBeTruthy();
245
+ });
246
+ });
247
+
248
+ describe("AssetMediaEngine", () => {
249
+ test("initializes with AssetMediaEngine", async ({
250
+ assetVideo,
251
+ expect,
252
+ }) => {
253
+ const mediaEngine = assetVideo.mediaEngineTask.value;
254
+ expect(mediaEngine).toBeInstanceOf(AssetMediaEngine);
255
+ expect(assetVideo.intrinsicDurationMs).toBeGreaterThan(0);
256
+ });
257
+
258
+ test("attempts thumbnail extraction (currently has implementation issues)", async ({
259
+ assetVideo,
260
+ expect,
261
+ }) => {
262
+ const mediaEngine = assetVideo.mediaEngineTask.value!;
263
+ expect(mediaEngine).toBeInstanceOf(AssetMediaEngine);
264
+
265
+ const timestamps = [2000]; // 2 seconds
266
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
267
+
268
+ expect(thumbnails).toHaveLength(1);
269
+
270
+ // NOTE: AssetMediaEngine thumbnail extraction currently has issues
271
+ // This test documents the current behavior for the refactor
272
+ if (thumbnails[0]) {
273
+ expect(thumbnails[0].timestamp).toBe(2000);
274
+ expect(thumbnails[0].thumbnail).toBeDefined();
275
+
276
+ const canvas = thumbnails[0].thumbnail;
277
+ expect(
278
+ canvas instanceof HTMLCanvasElement ||
279
+ canvas instanceof OffscreenCanvas,
280
+ ).toBe(true);
281
+ expect(canvas.width).toBeGreaterThan(0);
282
+ expect(canvas.height).toBeGreaterThan(0);
283
+ }
284
+ // If it returns null, that's also acceptable given current implementation issues
285
+ });
286
+
287
+ test("attempts batch thumbnail extraction (documents current behavior)", async ({
288
+ assetVideo,
289
+ expect,
290
+ }) => {
291
+ const mediaEngine = assetVideo.mediaEngineTask.value!;
292
+ expect(mediaEngine).toBeInstanceOf(AssetMediaEngine);
293
+
294
+ const timestamps = [1000, 3000, 5000, 7000]; // Multiple timestamps
295
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
296
+
297
+ expect(thumbnails).toHaveLength(4);
298
+
299
+ // Document current behavior - some may be null due to implementation issues
300
+ let successCount = 0;
301
+ for (let i = 0; i < timestamps.length; i++) {
302
+ const thumbnail = thumbnails[i];
303
+ if (thumbnail) {
304
+ successCount++;
305
+ expect(thumbnail.timestamp).toBe(timestamps[i]);
306
+
307
+ const canvas = thumbnail.thumbnail;
308
+ expect(
309
+ canvas instanceof HTMLCanvasElement ||
310
+ canvas instanceof OffscreenCanvas,
311
+ ).toBe(true);
312
+ expect(canvas.width).toBeGreaterThan(0);
313
+ expect(canvas.height).toBeGreaterThan(0);
314
+ }
315
+ }
316
+
317
+ // Track success rate for refactor planning
318
+ console.log(
319
+ `AssetMediaEngine batch extraction: ${successCount}/${timestamps.length} successful`,
320
+ );
321
+ });
322
+
323
+ test("documents that AssetMediaEngine is not yet supported", async ({
324
+ assetVideo,
325
+ expect,
326
+ }) => {
327
+ const mediaEngine = assetVideo.mediaEngineTask.value as AssetMediaEngine;
328
+ expect(mediaEngine).toBeInstanceOf(AssetMediaEngine);
329
+
330
+ // AssetMediaEngine now properly returns nulls for all requests
331
+ const timestamps = [500, 2500, 4500, 6500];
332
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
333
+
334
+ expect(thumbnails).toHaveLength(4);
335
+
336
+ // All should be null since AssetMediaEngine is not yet supported
337
+ const successfulThumbnails = thumbnails.filter((t) => t !== null);
338
+ expect(successfulThumbnails.length).toBe(0); // Consistent behavior now
339
+ });
340
+
341
+ test("handles invalid timestamps (reveals current boundary issues)", async ({
342
+ assetVideo,
343
+ expect,
344
+ }) => {
345
+ const mediaEngine = assetVideo.mediaEngineTask.value!;
346
+ expect(mediaEngine).toBeInstanceOf(AssetMediaEngine);
347
+
348
+ const timestamps = [-1000, 50000]; // Before start and well after end
349
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
350
+
351
+ expect(thumbnails).toHaveLength(2);
352
+
353
+ // Document current behavior - there seem to be boundary checking issues
354
+ // that should be fixed in the refactor
355
+ console.log(
356
+ `Invalid timestamps result: ${thumbnails[0] ? "non-null" : "null"}, ${thumbnails[1] ? "non-null" : "null"}`,
357
+ );
358
+
359
+ // At minimum, negative timestamps should be null
360
+ expect(thumbnails[0]).toBeNull();
361
+
362
+ // The second one might unexpectedly succeed due to current implementation issues
363
+ // This documents the current behavior for the refactor
364
+ if (thumbnails[1]) {
365
+ console.log(
366
+ "WARNING: Timestamp 50000ms unexpectedly returned a thumbnail - boundary checking issue",
367
+ );
368
+ }
369
+ });
370
+
371
+ test("no scrub rendition fallback to main video (documents current behavior)", async ({
372
+ assetVideo,
373
+ expect,
374
+ }) => {
375
+ const mediaEngine = assetVideo.mediaEngineTask.value as AssetMediaEngine;
376
+ expect(mediaEngine).toBeInstanceOf(AssetMediaEngine);
377
+
378
+ // AssetMediaEngine doesn't have scrub rendition
379
+ const scrubRendition = mediaEngine.getScrubVideoRendition();
380
+ expect(scrubRendition).toBeUndefined();
381
+
382
+ // Attempt to extract thumbnails using main video rendition
383
+ const timestamps = [2000];
384
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
385
+
386
+ expect(thumbnails).toHaveLength(1);
387
+
388
+ // Document current behavior - may return null due to implementation issues
389
+ if (thumbnails[0]) {
390
+ expect(thumbnails[0].timestamp).toBe(2000);
391
+ expect(thumbnails[0].thumbnail).toBeDefined();
392
+ } else {
393
+ console.log(
394
+ "AssetMediaEngine fallback to main video rendition currently returns null",
395
+ );
396
+ }
397
+ });
398
+
399
+ test("documents segment boundary behavior for refactor", async ({
400
+ assetVideo,
401
+ expect,
402
+ }) => {
403
+ const mediaEngine = assetVideo.mediaEngineTask.value as AssetMediaEngine;
404
+ expect(mediaEngine).toBeInstanceOf(AssetMediaEngine);
405
+
406
+ // Test around known segment boundaries from bars-n-tone.mp4
407
+ // These are approximate - the actual boundaries depend on the asset
408
+ const timestamps = [2066, 4033, 6066, 8033];
409
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
410
+
411
+ expect(thumbnails).toHaveLength(4);
412
+
413
+ // Document current success rate for refactor planning
414
+ const successfulThumbnails = thumbnails.filter((t) => t !== null);
415
+ console.log(
416
+ `AssetMediaEngine segment boundary test: ${successfulThumbnails.length}/${timestamps.length} successful`,
417
+ );
418
+
419
+ // Current implementation may have issues - document for refactor
420
+ // In an ideal implementation, most of these should succeed
421
+ for (const thumbnail of successfulThumbnails) {
422
+ expect(thumbnail.thumbnail).toBeDefined();
423
+ const canvas = thumbnail.thumbnail;
424
+ expect(
425
+ canvas instanceof HTMLCanvasElement ||
426
+ canvas instanceof OffscreenCanvas,
427
+ ).toBe(true);
428
+ }
429
+ });
430
+ });
431
+
432
+ describe("AssetMediaEngine Incompatibility Warning", () => {
433
+ test("logs warning when EFThumbnailStrip targets AssetMediaEngine", async ({
434
+ assetVideo,
435
+ expect,
436
+ }) => {
437
+ // Spy on console.warn to capture the warning
438
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
439
+
440
+ // Create a thumbnail strip and add it to DOM so it gets properly initialized
441
+ const thumbnailStrip = document.createElement("ef-thumbnail-strip");
442
+ thumbnailStrip.thumbnailWidth = 80;
443
+ document.body.appendChild(thumbnailStrip);
444
+
445
+ // Wait for both elements to complete their setup
446
+ await Promise.all([
447
+ assetVideo.updateComplete,
448
+ thumbnailStrip.updateComplete,
449
+ ]);
450
+
451
+ // Directly set the target element to bypass TargetController complexity in tests
452
+ assetVideo.id = "asset-video"; // For the warning message
453
+ thumbnailStrip.targetElement = assetVideo;
454
+
455
+ // Trigger the layout task through the normal flow by setting stripWidth
456
+ // This mimics what ResizeObserver would do and triggers the warning
457
+ (thumbnailStrip as any).stripWidth = 400;
458
+
459
+ // Wait for the warning to be logged using vi.waitFor for event-driven testing
460
+ await vi.waitFor(
461
+ () => {
462
+ expect(consoleSpy).toHaveBeenCalledWith(
463
+ expect.stringContaining(
464
+ "AssetMediaEngine: extractThumbnails not properly implemented",
465
+ ),
466
+ );
467
+ },
468
+ { timeout: 2000 },
469
+ );
470
+
471
+ // Clean up
472
+ thumbnailStrip.remove();
473
+
474
+ // Restore console.warn
475
+ consoleSpy.mockRestore();
476
+ });
477
+
478
+ test("does NOT log warning when EFThumbnailStrip targets JitMediaEngine", async ({
479
+ jitVideo,
480
+ expect,
481
+ }) => {
482
+ // Spy on console.warn to ensure no warning is logged
483
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
484
+
485
+ // Create a thumbnail strip and add it to DOM so it gets properly initialized
486
+ const thumbnailStrip = document.createElement("ef-thumbnail-strip");
487
+ thumbnailStrip.thumbnailWidth = 80;
488
+ document.body.appendChild(thumbnailStrip);
489
+
490
+ // Wait for elements to complete setup
491
+ await Promise.all([
492
+ jitVideo.updateComplete,
493
+ thumbnailStrip.updateComplete,
494
+ ]);
495
+
496
+ // Directly set the target element to bypass TargetController complexity in tests
497
+ jitVideo.id = "jit-video"; // For consistency
498
+ thumbnailStrip.targetElement = jitVideo;
499
+
500
+ // Trigger the layout task through the normal flow by setting stripWidth
501
+ (thumbnailStrip as any).stripWidth = 400;
502
+
503
+ // Wait for the layout task to complete using vi.waitFor
504
+ await vi.waitFor(
505
+ () => {
506
+ // @ts-expect-error testing private task
507
+ const layout = thumbnailStrip.thumbnailLayoutTask?.value;
508
+ expect(layout?.count).toBeGreaterThan(0);
509
+ },
510
+ { timeout: 2000 },
511
+ );
512
+
513
+ // Check that NO AssetMediaEngine warning was logged
514
+ const warningCalls = consoleSpy.mock.calls.filter((call) =>
515
+ call[0].includes("AssetMediaEngine is not currently supported"),
516
+ );
517
+ expect(warningCalls).toHaveLength(0);
518
+
519
+ // Clean up
520
+ thumbnailStrip.remove();
521
+
522
+ // Restore console.warn
523
+ consoleSpy.mockRestore();
524
+ });
525
+ });
526
+
527
+ describe("Caching Behavior", () => {
528
+ test("global input cache is accessible for debugging", async ({
529
+ expect,
530
+ }) => {
531
+ // Verify that the global Input cache is accessible
532
+ expect((globalThis as any).debugInputCache).toBeDefined();
533
+
534
+ const cache = (globalThis as any).debugInputCache;
535
+ expect(cache.getStats).toBeDefined();
536
+ expect(cache.clear).toBeDefined();
537
+ });
538
+
539
+ test("input instances are cached globally for efficiency", async ({
540
+ jitVideo,
541
+ expect,
542
+ }) => {
543
+ const mediaEngine = jitVideo.mediaEngineTask.value as JitMediaEngine;
544
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
545
+
546
+ // Clear cache to start fresh
547
+ (globalThis as any).debugInputCache.clear();
548
+
549
+ // Extract thumbnails from same segment multiple times
550
+ const firstBatch = await mediaEngine.extractThumbnails([1000, 1500]);
551
+ const secondBatch = await mediaEngine.extractThumbnails([1200, 1800]); // Same segment
552
+
553
+ expect(firstBatch).toHaveLength(2);
554
+ expect(secondBatch).toHaveLength(2);
555
+
556
+ // All should succeed
557
+ expect(firstBatch.every((t) => t !== null)).toBe(true);
558
+ expect(secondBatch.every((t) => t !== null)).toBe(true);
559
+
560
+ // Verify that Input objects are being cached globally
561
+ const cacheStats = (globalThis as any).debugInputCache.getStats();
562
+ expect(cacheStats.size).toBeGreaterThan(0);
563
+ console.log("Global Input cache stats:", cacheStats);
564
+ });
565
+
566
+ test("different segments create separate input cache entries", async ({
567
+ jitVideo,
568
+ expect,
569
+ }) => {
570
+ const mediaEngine = jitVideo.mediaEngineTask.value as JitMediaEngine;
571
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
572
+
573
+ // Extract thumbnails from different segments
574
+ const segment1 = await mediaEngine.extractThumbnails([1000]);
575
+ const segment2 = await mediaEngine.extractThumbnails([3000]); // Different segment
576
+ const segment3 = await mediaEngine.extractThumbnails([5000]); // Different segment
577
+
578
+ expect(segment1).toHaveLength(1);
579
+ expect(segment2).toHaveLength(1);
580
+ expect(segment3).toHaveLength(1);
581
+
582
+ // All should succeed
583
+ expect(segment1[0]).toBeTruthy();
584
+ expect(segment2[0]).toBeTruthy();
585
+ expect(segment3[0]).toBeTruthy();
586
+ });
587
+ });
588
+
589
+ describe("Error Handling", () => {
590
+ test("handles media engine without video track", async ({ expect }) => {
591
+ // Create a video element but don't wait for it to fully load
592
+ const video = document.createElement("ef-video");
593
+ video.src = "nonexistent.mp4";
594
+ document.body.appendChild(video);
595
+
596
+ try {
597
+ await video.mediaEngineTask.run();
598
+ const mediaEngine = video.mediaEngineTask.value;
599
+
600
+ if (mediaEngine) {
601
+ const timestamps = [1000];
602
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
603
+
604
+ // Should handle gracefully with nulls
605
+ expect(thumbnails).toHaveLength(1);
606
+ expect(thumbnails[0]).toBeNull();
607
+ }
608
+ } catch (error) {
609
+ // Media engine creation might fail for nonexistent file - that's expected
610
+ expect(error).toBeDefined();
611
+ } finally {
612
+ video.remove();
613
+ }
614
+ });
615
+
616
+ test("handles concurrent thumbnail extraction requests", async ({
617
+ jitVideo,
618
+ expect,
619
+ }) => {
620
+ const mediaEngine = jitVideo.mediaEngineTask.value!;
621
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
622
+
623
+ // Start multiple concurrent extractions
624
+ const promise1 = mediaEngine.extractThumbnails([1000, 2000]);
625
+ const promise2 = mediaEngine.extractThumbnails([3000, 4000]);
626
+ const promise3 = mediaEngine.extractThumbnails([5000, 6000]);
627
+
628
+ const [result1, result2, result3] = await Promise.all([
629
+ promise1,
630
+ promise2,
631
+ promise3,
632
+ ]);
633
+
634
+ expect(result1).toHaveLength(2);
635
+ expect(result2).toHaveLength(2);
636
+ expect(result3).toHaveLength(2);
637
+
638
+ // All should succeed
639
+ expect(result1.every((t) => t !== null)).toBe(true);
640
+ expect(result2.every((t) => t !== null)).toBe(true);
641
+ expect(result3.every((t) => t !== null)).toBe(true);
642
+ });
643
+ });
644
+
645
+ describe("Performance Characteristics", () => {
646
+ test("batch extraction is more efficient than individual calls", async ({
647
+ jitVideo,
648
+ expect,
649
+ }) => {
650
+ const mediaEngine = jitVideo.mediaEngineTask.value!;
651
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
652
+
653
+ const timestamps = [1000, 2000, 3000, 4000];
654
+
655
+ // Time batch extraction
656
+ const batchStart = performance.now();
657
+ const batchResults = await mediaEngine.extractThumbnails(timestamps);
658
+ const batchEnd = performance.now();
659
+
660
+ // Time individual extractions
661
+ const individualStart = performance.now();
662
+ const individualResults = [];
663
+ for (const timestamp of timestamps) {
664
+ const result = await mediaEngine.extractThumbnails([timestamp]);
665
+ individualResults.push(result[0]);
666
+ }
667
+ const individualEnd = performance.now();
668
+
669
+ const batchTime = batchEnd - batchStart;
670
+ const individualTime = individualEnd - individualStart;
671
+
672
+ expect(batchResults).toHaveLength(4);
673
+ expect(individualResults).toHaveLength(4);
674
+
675
+ // Results should be equivalent
676
+ for (let i = 0; i < timestamps.length; i++) {
677
+ expect(batchResults[i]?.timestamp).toBe(
678
+ individualResults[i]?.timestamp,
679
+ );
680
+ }
681
+
682
+ console.log(
683
+ `Batch time: ${batchTime.toFixed(2)}ms, Individual time: ${individualTime.toFixed(2)}ms`,
684
+ );
685
+
686
+ // Batch should generally be faster (though this might vary in test environments)
687
+ // We don't enforce this as a hard requirement since test timing can be variable
688
+ expect(batchTime).toBeGreaterThan(0);
689
+ expect(individualTime).toBeGreaterThan(0);
690
+ });
691
+
692
+ test("segment grouping optimizes cross-segment extraction", async ({
693
+ jitVideo,
694
+ expect,
695
+ }) => {
696
+ const mediaEngine = jitVideo.mediaEngineTask.value!;
697
+ expect(mediaEngine).toBeInstanceOf(JitMediaEngine);
698
+
699
+ // Extract thumbnails that span multiple segments but in an order
700
+ // that would be inefficient without segment grouping
701
+ const timestamps = [1000, 5000, 1500, 5500, 2000, 6000]; // Alternating segments
702
+ const thumbnails = await mediaEngine.extractThumbnails(timestamps);
703
+
704
+ expect(thumbnails).toHaveLength(6);
705
+
706
+ // All should succeed despite the inefficient ordering
707
+ for (let i = 0; i < timestamps.length; i++) {
708
+ expect(thumbnails[i]).toBeTruthy();
709
+ expect(thumbnails[i]?.timestamp).toBe(timestamps[i]);
710
+ }
711
+ });
712
+ });
713
+ });