@editframe/elements 0.19.4-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 +6 -1
  28. package/dist/elements/EFTimegroup.js +46 -10
  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 +70 -11
  78. package/src/elements/updateAnimations.browsertest.ts +333 -11
  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,591 @@
1
+ import { html, render } from "lit";
2
+ import { beforeEach, describe } from "vitest";
3
+
4
+ import { test as baseTest } from "../../test/useMSW.js";
5
+ import "../gui/EFPreview.js";
6
+ import "../gui/EFWorkbench.js";
7
+ import "./EFThumbnailStrip.js";
8
+ import type { EFThumbnailStrip } from "./EFThumbnailStrip.js";
9
+ import "./EFTimegroup.js";
10
+ import "./EFVideo.js";
11
+
12
+ import type { EFTimegroup } from "./EFTimegroup.js";
13
+ import type { EFVideo } from "./EFVideo.js";
14
+
15
+ beforeEach(async () => {
16
+ localStorage.clear();
17
+ await fetch("/@ef-clear-cache", { method: "DELETE" });
18
+ });
19
+
20
+ // IMPLEMENTATION GUIDELINES: Test initial thumbnail loading without requiring resize events
21
+
22
+ interface ThumbnailStripFixture {
23
+ video: EFVideo;
24
+ thumbnailStrip: EFThumbnailStrip;
25
+ timegroup: EFTimegroup;
26
+ container: HTMLElement;
27
+ }
28
+
29
+ const test = baseTest.extend<{
30
+ thumbnailStripSetup: ThumbnailStripFixture;
31
+ alternateSetup: ThumbnailStripFixture;
32
+ }>({
33
+ thumbnailStripSetup: async ({}, use) => {
34
+ const container = document.createElement("div");
35
+ render(
36
+ html`
37
+ <ef-configuration api-host="http://localhost:63315">
38
+ <div style="width: 600px; height: 400px;">
39
+ <ef-preview class="w-[600px] h-[300px]">
40
+ <ef-timegroup mode="contain" class="w-full h-full bg-black">
41
+ <ef-video src="http://web:3000/head-moov-480p.mp4" id="test-video" class="size-full object-contain"></ef-video>
42
+ </ef-timegroup>
43
+ </ef-preview>
44
+ <ef-thumbnail-strip
45
+ target="test-video"
46
+ thumbnail-width="80"
47
+ class="w-full"
48
+ style="height: 48px;"
49
+ ></ef-thumbnail-strip>
50
+ </div>
51
+ </ef-configuration>
52
+ `,
53
+ container,
54
+ );
55
+ document.body.appendChild(container);
56
+
57
+ const config = container.querySelector("ef-configuration") as any;
58
+ config.signingURL = "";
59
+
60
+ const timegroup = container.querySelector("ef-timegroup") as EFTimegroup;
61
+ const video = container.querySelector("ef-video") as EFVideo;
62
+ const thumbnailStrip = container.querySelector(
63
+ "ef-thumbnail-strip",
64
+ ) as EFThumbnailStrip;
65
+
66
+ await Promise.all([
67
+ timegroup.updateComplete,
68
+ video.updateComplete,
69
+ thumbnailStrip.updateComplete,
70
+ ]);
71
+
72
+ await use({ video, thumbnailStrip, timegroup, container });
73
+ container.remove();
74
+ },
75
+ alternateSetup: async ({}, use) => {
76
+ const container = document.createElement("div");
77
+ render(
78
+ html`
79
+ <ef-configuration api-host="http://localhost:63315">
80
+ <div style="width: 400px; height: 300px;">
81
+ <ef-preview class="w-full h-[200px]">
82
+ <ef-timegroup mode="contain" class="w-full h-full bg-black">
83
+ <ef-video src="http://web:3000/head-moov-480p.mp4" id="alt-video" class="size-full object-contain"></ef-video>
84
+ </ef-timegroup>
85
+ </ef-preview>
86
+ <ef-thumbnail-strip
87
+ target="alt-video"
88
+ thumbnail-width="80"
89
+ class="w-full"
90
+ style="height: 48px;"
91
+ ></ef-thumbnail-strip>
92
+ </div>
93
+ </ef-configuration>
94
+ `,
95
+ container,
96
+ );
97
+ document.body.appendChild(container);
98
+
99
+ const config = container.querySelector("ef-configuration") as any;
100
+ config.signingURL = "";
101
+
102
+ const timegroup = container.querySelector("ef-timegroup") as EFTimegroup;
103
+ const video = container.querySelector("ef-video") as EFVideo;
104
+ const thumbnailStrip = container.querySelector(
105
+ "ef-thumbnail-strip",
106
+ ) as EFThumbnailStrip;
107
+
108
+ await Promise.all([
109
+ timegroup.updateComplete,
110
+ video.updateComplete,
111
+ thumbnailStrip.updateComplete,
112
+ ]);
113
+
114
+ await use({ video, thumbnailStrip, timegroup, container });
115
+ container.remove();
116
+ },
117
+ });
118
+
119
+ const awaitThumbnailLayout = async (thumbnailStrip: EFThumbnailStrip) => {
120
+ // @ts-expect-error missing implementation
121
+ await thumbnailStrip.thumbnailLayoutTask.taskComplete;
122
+ };
123
+
124
+ describe("EFThumbnailStrip", () => {
125
+ describe("initialization", () => {
126
+ test("should detect dimensions and target element on connection", async ({
127
+ expect,
128
+ thumbnailStripSetup,
129
+ }) => {
130
+ const { video, thumbnailStrip } = thumbnailStripSetup;
131
+
132
+ await video.mediaEngineTask.taskComplete;
133
+
134
+ expect(thumbnailStrip.targetElement).toBe(video);
135
+ // @ts-expect-error testing private property
136
+ expect(thumbnailStrip.stripWidth).toBeGreaterThan(0);
137
+
138
+ const canvas = thumbnailStrip.shadowRoot?.querySelector("canvas");
139
+ expect(canvas).toBeTruthy();
140
+ expect(canvas?.width).toBeGreaterThan(0);
141
+ expect(canvas?.height).toBeGreaterThan(0);
142
+ }, 1000);
143
+
144
+ test("should select target element by ID", async ({
145
+ expect,
146
+ thumbnailStripSetup,
147
+ }) => {
148
+ const { video, thumbnailStrip } = thumbnailStripSetup;
149
+
150
+ expect(thumbnailStrip.targetElement).toBe(video);
151
+ expect(thumbnailStrip.target).toBe("test-video");
152
+ }, 1000);
153
+ });
154
+
155
+ describe("trimmed duration behavior", () => {
156
+ test("should show thumbnails from trimmed time range by default", async ({
157
+ expect,
158
+ thumbnailStripSetup,
159
+ }) => {
160
+ const { video, thumbnailStrip } = thumbnailStripSetup;
161
+
162
+ // Set trim properties on the video
163
+ video.setAttribute("trimstart", "2s");
164
+ video.setAttribute("trimend", "1s");
165
+ await video.updateComplete;
166
+ await video.mediaEngineTask.taskComplete;
167
+
168
+ // Wait for thumbnail layout to complete
169
+ await awaitThumbnailLayout(thumbnailStrip);
170
+
171
+ // Video should have trimmed duration
172
+ expect(video.sourceStartMs).toBe(2000); // trimstart 2s
173
+ expect(video.durationMs).toBe(7000); // 10s - 2s trimstart - 1s trimend
174
+
175
+ // Thumbnails should be generated for the trimmed range by default (not intrinsic)
176
+ expect(thumbnailStrip.useIntrinsicDuration).toBe(false);
177
+ }, 1000);
178
+
179
+ test("should recalculate thumbnails when trim properties change dynamically", async ({
180
+ expect,
181
+ thumbnailStripSetup,
182
+ }) => {
183
+ const { video, thumbnailStrip } = thumbnailStripSetup;
184
+
185
+ await video.mediaEngineTask.taskComplete;
186
+
187
+ // Start with no trimming - should use full duration
188
+ expect(video.sourceStartMs).toBe(0);
189
+ expect(video.durationMs).toBe(10000); // full 10s duration
190
+
191
+ // Wait for initial thumbnail layout
192
+ await awaitThumbnailLayout(thumbnailStrip);
193
+
194
+ // Now add trim properties dynamically
195
+ video.setAttribute("trimstart", "3s");
196
+ video.setAttribute("trimend", "2s");
197
+ await video.updateComplete;
198
+
199
+ // @ts-expect-error testing private task
200
+ await thumbnailStrip.thumbnailLayoutTask.taskComplete;
201
+
202
+ // Wait for the thumbnail update to complete
203
+ await awaitThumbnailLayout(thumbnailStrip);
204
+
205
+ // Video should now have updated trimmed values
206
+ expect(video.sourceStartMs).toBe(3000); // trimstart 3s
207
+ expect(video.durationMs).toBe(5000); // 10s - 3s trimstart - 2s trimend
208
+
209
+ // Change trim properties again
210
+ video.setAttribute("trimstart", "1s");
211
+ video.setAttribute("trimend", "1s");
212
+ await video.updateComplete;
213
+
214
+ // Wait for the second thumbnail update
215
+ await awaitThumbnailLayout(thumbnailStrip);
216
+
217
+ // Video should reflect the new trim values
218
+ expect(video.sourceStartMs).toBe(1000); // trimstart 1s
219
+ expect(video.durationMs).toBe(8000); // 10s - 1s trimstart - 1s trimend
220
+ }, 2000);
221
+
222
+ test("should recalculate thumbnails when sourcein/sourceout properties change", async ({
223
+ expect,
224
+ thumbnailStripSetup,
225
+ }) => {
226
+ const { video, thumbnailStrip } = thumbnailStripSetup;
227
+
228
+ await video.mediaEngineTask.taskComplete;
229
+
230
+ // Start with sourcein/sourceout properties
231
+ video.setAttribute("sourcein", "2s");
232
+ video.setAttribute("sourceout", "8s");
233
+ await video.updateComplete;
234
+
235
+ // Wait for thumbnail layout
236
+ await awaitThumbnailLayout(thumbnailStrip);
237
+
238
+ // Video should have source-based duration
239
+ expect(video.sourceStartMs).toBe(2000); // sourcein 2s
240
+ expect(video.durationMs).toBe(6000); // sourceout 8s - sourcein 2s
241
+
242
+ // Change the source properties
243
+ video.setAttribute("sourcein", "1s");
244
+ video.setAttribute("sourceout", "9s");
245
+ await video.updateComplete;
246
+
247
+ // Wait for thumbnail update
248
+ await awaitThumbnailLayout(thumbnailStrip);
249
+
250
+ // Video should reflect new source values
251
+ expect(video.sourceStartMs).toBe(1000); // sourcein 1s
252
+ expect(video.durationMs).toBe(8000); // sourceout 9s - sourcein 1s
253
+ }, 2000);
254
+
255
+ test("should show thumbnails from full duration when useIntrinsicDuration is true", async ({
256
+ expect,
257
+ thumbnailStripSetup,
258
+ }) => {
259
+ const { video, thumbnailStrip } = thumbnailStripSetup;
260
+
261
+ // Set trim properties and useIntrinsicDuration
262
+ video.setAttribute("trimstart", "2s");
263
+ video.setAttribute("trimend", "1s");
264
+ thumbnailStrip.setAttribute("use-intrinsic-duration", "true");
265
+
266
+ await Promise.all([video.updateComplete, thumbnailStrip.updateComplete]);
267
+ await video.mediaEngineTask.taskComplete;
268
+
269
+ // Wait for thumbnail layout to complete
270
+ await awaitThumbnailLayout(thumbnailStrip);
271
+
272
+ // Video should have trimmed duration but thumbnail strip uses intrinsic
273
+ expect(video.sourceStartMs).toBe(2000); // trimstart 2s
274
+ expect(video.durationMs).toBe(7000); // trimmed duration (10s - 2s - 1s)
275
+ expect(video.intrinsicDurationMs).toBe(10000); // full duration
276
+ expect(thumbnailStrip.useIntrinsicDuration).toBe(true);
277
+ }, 1000);
278
+
279
+ test("should ignore all trim properties when useIntrinsicDuration is true", async ({
280
+ expect,
281
+ thumbnailStripSetup,
282
+ }) => {
283
+ const { video, thumbnailStrip } = thumbnailStripSetup;
284
+
285
+ await video.mediaEngineTask.taskComplete;
286
+
287
+ // First verify default trimmed behavior
288
+ video.setAttribute("trimstart", "3s");
289
+ video.setAttribute("trimend", "2s");
290
+ await video.updateComplete;
291
+
292
+ await awaitThumbnailLayout(thumbnailStrip);
293
+
294
+ // Should respect trim by default
295
+ expect(video.sourceStartMs).toBe(3000); // trimstart 3s
296
+ expect(video.durationMs).toBe(5000); // 10s - 3s - 2s
297
+
298
+ // Now enable useIntrinsicDuration
299
+ thumbnailStrip.setAttribute("use-intrinsic-duration", "true");
300
+ await thumbnailStrip.updateComplete;
301
+
302
+ await awaitThumbnailLayout(thumbnailStrip);
303
+
304
+ // Video properties should still reflect trim settings
305
+ expect(video.sourceStartMs).toBe(3000); // trimstart 3s
306
+ expect(video.durationMs).toBe(5000); // trimmed duration
307
+ expect(video.intrinsicDurationMs).toBe(10000); // full duration
308
+
309
+ // But thumbnail strip should ignore trims and use full duration
310
+ expect(thumbnailStrip.useIntrinsicDuration).toBe(true);
311
+
312
+ // The layout calculation should use 0 to intrinsicDurationMs instead of trimmed range
313
+ // We can verify this by checking that the implementation correctly handles the flag
314
+ }, 2000);
315
+
316
+ test("should handle custom start-time-ms/end-time-ms relative to correct timeline", async ({
317
+ expect,
318
+ thumbnailStripSetup,
319
+ }) => {
320
+ const { video, thumbnailStrip } = thumbnailStripSetup;
321
+
322
+ // Set up trim: source 0-10s becomes trimmed 0-7s (source 2-9s)
323
+ video.setAttribute("trimstart", "2s");
324
+ video.setAttribute("trimend", "1s");
325
+ await video.updateComplete;
326
+ // CRITICAL: Wait for media engine task completion AFTER setting trim attributes
327
+ await video.mediaEngineTask.taskComplete;
328
+
329
+ // Verify base setup
330
+ expect(video.sourceStartMs).toBe(2000); // Trim starts at 2s in source
331
+ expect(video.durationMs).toBe(7000); // 7s trimmed duration
332
+
333
+ // Test custom time range in TRIMMED mode (default)
334
+ // start-time-ms="1000" should mean 1s into the trimmed portion = 3s in source
335
+ // end-time-ms="5000" should mean 5s into the trimmed portion = 7s in source
336
+ thumbnailStrip.setAttribute("start-time-ms", "1000"); // 1s into trimmed timeline
337
+ thumbnailStrip.setAttribute("end-time-ms", "5000"); // 5s into trimmed timeline
338
+ await thumbnailStrip.updateComplete;
339
+
340
+ // Verify the properties were set
341
+ expect(thumbnailStrip.startTimeMs).toBe(1000);
342
+ expect(thumbnailStrip.endTimeMs).toBe(5000);
343
+
344
+ // Force thumbnail layout to run with new properties
345
+ // @ts-expect-error missing implementation
346
+ thumbnailStrip.thumbnailLayoutTask.run();
347
+ await awaitThumbnailLayout(thumbnailStrip);
348
+
349
+ // Get layout and check timestamps
350
+ // @ts-expect-error missing implementation
351
+ const layout = thumbnailStrip.thumbnailLayoutTask.value;
352
+ expect(layout).toBeTruthy();
353
+
354
+ if (layout) {
355
+ const allTimestamps = layout.segments.flatMap((segment) =>
356
+ segment.thumbnails.map((thumb) => thumb.timeMs),
357
+ );
358
+
359
+ expect(allTimestamps.length).toBeGreaterThan(0);
360
+
361
+ // Thumbnails should be from 3000ms to 7000ms in source timeline
362
+ // (trimstart 2000ms + custom start 1000ms = 3000ms to trimstart 2000ms + custom end 5000ms = 7000ms)
363
+ const minTime = Math.min(...allTimestamps);
364
+ const maxTime = Math.max(...allTimestamps);
365
+
366
+ expect(minTime).toBeGreaterThanOrEqual(3000); // Should start from 3s in source
367
+ expect(minTime).toBeLessThan(3500); // Should be close to 3s
368
+ expect(maxTime).toBeLessThanOrEqual(7000); // Should end at 7s in source
369
+ expect(maxTime).toBeGreaterThan(6500); // Should be close to 7s
370
+ }
371
+
372
+ // Test INTRINSIC mode with custom times
373
+ // start-time-ms="1000" should mean 1s in source timeline
374
+ // end-time-ms="8000" should mean 8s in source timeline
375
+ thumbnailStrip.setAttribute("use-intrinsic-duration", "true");
376
+ thumbnailStrip.setAttribute("start-time-ms", "1000"); // 1s in source timeline
377
+ thumbnailStrip.setAttribute("end-time-ms", "8000"); // 8s in source timeline
378
+ await thumbnailStrip.updateComplete;
379
+
380
+ // Verify the properties were set correctly for intrinsic mode
381
+ expect(thumbnailStrip.useIntrinsicDuration).toBe(true);
382
+ expect(thumbnailStrip.startTimeMs).toBe(1000);
383
+ expect(thumbnailStrip.endTimeMs).toBe(8000);
384
+
385
+ // Force thumbnail layout to run with intrinsic mode properties
386
+ // @ts-expect-error missing implementation
387
+ thumbnailStrip.thumbnailLayoutTask.run();
388
+ await awaitThumbnailLayout(thumbnailStrip);
389
+
390
+ // @ts-expect-error missing implementation
391
+ const intrinsicLayout = thumbnailStrip.thumbnailLayoutTask.value;
392
+ expect(intrinsicLayout).toBeTruthy();
393
+
394
+ if (intrinsicLayout) {
395
+ const intrinsicTimestamps = intrinsicLayout.segments.flatMap(
396
+ (segment) => segment.thumbnails.map((thumb) => thumb.timeMs),
397
+ );
398
+
399
+ expect(intrinsicTimestamps.length).toBeGreaterThan(0);
400
+
401
+ // In intrinsic mode, should be 1000ms to 8000ms directly in source timeline
402
+ const intrinsicMin = Math.min(...intrinsicTimestamps);
403
+ const intrinsicMax = Math.max(...intrinsicTimestamps);
404
+
405
+ expect(intrinsicMin).toBeGreaterThanOrEqual(1000); // Should start from 1s in source
406
+ expect(intrinsicMin).toBeLessThan(1500); // Should be close to 1s
407
+ expect(intrinsicMax).toBeLessThanOrEqual(8000); // Should end at 8s in source
408
+ expect(intrinsicMax).toBeGreaterThan(7500); // Should be close to 8s
409
+ }
410
+ }, 3000);
411
+
412
+ test("should correctly parse use-intrinsic-duration string values", async ({
413
+ expect,
414
+ thumbnailStripSetup,
415
+ }) => {
416
+ const { thumbnailStrip } = thumbnailStripSetup;
417
+
418
+ // Test default value
419
+ expect(thumbnailStrip.useIntrinsicDuration).toBe(false);
420
+
421
+ // Test "true" string value
422
+ thumbnailStrip.setAttribute("use-intrinsic-duration", "true");
423
+ await thumbnailStrip.updateComplete;
424
+ expect(thumbnailStrip.useIntrinsicDuration).toBe(true);
425
+
426
+ // Test "false" string value (this is the key fix)
427
+ thumbnailStrip.setAttribute("use-intrinsic-duration", "false");
428
+ await thumbnailStrip.updateComplete;
429
+ expect(thumbnailStrip.useIntrinsicDuration).toBe(false); // Should be false, not true!
430
+
431
+ // Test removing attribute
432
+ thumbnailStrip.removeAttribute("use-intrinsic-duration");
433
+ await thumbnailStrip.updateComplete;
434
+ expect(thumbnailStrip.useIntrinsicDuration).toBe(false);
435
+
436
+ // Test setting via property
437
+ thumbnailStrip.useIntrinsicDuration = true;
438
+ await thumbnailStrip.updateComplete;
439
+ expect(thumbnailStrip.getAttribute("use-intrinsic-duration")).toBe(
440
+ "true",
441
+ );
442
+
443
+ thumbnailStrip.useIntrinsicDuration = false;
444
+ await thumbnailStrip.updateComplete;
445
+ expect(thumbnailStrip.hasAttribute("use-intrinsic-duration")).toBe(false);
446
+ }, 1000);
447
+
448
+ test("should show trimmed thumbnails when use-intrinsic-duration='false'", async ({
449
+ expect,
450
+ thumbnailStripSetup,
451
+ }) => {
452
+ const { video, thumbnailStrip } = thumbnailStripSetup;
453
+
454
+ // Set trim and explicitly set use-intrinsic-duration="false"
455
+ video.setAttribute("trimstart", "2s");
456
+ thumbnailStrip.setAttribute("use-intrinsic-duration", "false"); // This should be false!
457
+
458
+ await video.updateComplete;
459
+ await thumbnailStrip.updateComplete;
460
+ await video.mediaEngineTask.taskComplete;
461
+
462
+ await awaitThumbnailLayout(thumbnailStrip);
463
+
464
+ // Verify the boolean parsing worked correctly
465
+ expect(thumbnailStrip.useIntrinsicDuration).toBe(false); // Should be false, not true!
466
+ expect(thumbnailStrip.getAttribute("use-intrinsic-duration")).toBe(
467
+ "false",
468
+ );
469
+
470
+ // Verify thumbnail behavior: should use trimmed timeline, starting from 2s
471
+ expect(video.sourceStartMs).toBe(2000); // trimstart 2s
472
+ expect(video.durationMs).toBe(8000); // 10s - 2s = 8s trimmed duration
473
+
474
+ // @ts-expect-error testing private task
475
+ const layout = thumbnailStrip.thumbnailLayoutTask.value;
476
+ if (layout) {
477
+ const allTimestamps = layout.segments.flatMap((segment) =>
478
+ segment.thumbnails.map((thumb) => thumb.timeMs),
479
+ );
480
+ expect(allTimestamps.length).toBeGreaterThan(0);
481
+
482
+ // Thumbnails should start from 2000ms (trimstart), not 0ms
483
+ const firstTimestamp = Math.min(...allTimestamps);
484
+ expect(firstTimestamp).toBeGreaterThanOrEqual(2000);
485
+ expect(firstTimestamp).toBeLessThan(2500);
486
+ }
487
+ }, 1000);
488
+
489
+ test("should align first thumbnail with video currentTime=0 frame", async ({
490
+ expect,
491
+ thumbnailStripSetup,
492
+ }) => {
493
+ const { video, thumbnailStrip } = thumbnailStripSetup;
494
+
495
+ // Set trim to 2s - both video and thumbnails should show same frame
496
+ video.setAttribute("trimstart", "2s");
497
+ video.setAttribute("current-time", "0"); // 0 in trimmed timeline = 2s in source
498
+
499
+ await video.updateComplete;
500
+ await video.mediaEngineTask.taskComplete;
501
+
502
+ // Wait for thumbnail calculations
503
+ // @ts-expect-error testing private task
504
+ await thumbnailStrip.thumbnailLayoutTask.taskComplete;
505
+
506
+ // Get the video's current source time (what frame it's showing)
507
+ const videoSourceTime = video.sourceStartMs + (video.currentTimeMs || 0);
508
+ expect(videoSourceTime).toBe(2000); // Should be at 2s in source (frame 61)
509
+
510
+ // Get the first thumbnail timestamp
511
+ // @ts-expect-error testing private task
512
+ const layout = thumbnailStrip.thumbnailLayoutTask.value;
513
+ if (layout) {
514
+ const allTimestamps = layout.segments.flatMap((segment) =>
515
+ segment.thumbnails.map((thumb) => thumb.timeMs),
516
+ );
517
+ expect(allTimestamps.length).toBeGreaterThan(0);
518
+
519
+ const firstThumbnailTime = Math.min(...allTimestamps);
520
+
521
+ // The first thumbnail should be at exactly the same source time as video currentTime=0
522
+ // This ensures frame 61 shows in both video and thumbnail
523
+ expect(firstThumbnailTime).toBe(videoSourceTime); // Should be exactly 2000ms
524
+ }
525
+ }, 1000);
526
+ });
527
+
528
+ describe("layout behavior", () => {
529
+ test("should calculate layout immediately after initialization", async ({
530
+ expect,
531
+ alternateSetup,
532
+ }) => {
533
+ const { video, thumbnailStrip } = alternateSetup;
534
+
535
+ await video.mediaEngineTask.taskComplete;
536
+
537
+ // @ts-expect-error testing private property
538
+ expect(thumbnailStrip.stripWidth).toBe(400); // Uses container inner width
539
+ expect(thumbnailStrip.targetElement).toBe(video);
540
+ }, 1000);
541
+
542
+ test("should remain stable when video properties change", async ({
543
+ expect,
544
+ thumbnailStripSetup,
545
+ }) => {
546
+ const { video, thumbnailStrip } = thumbnailStripSetup;
547
+
548
+ await video.mediaEngineTask.taskComplete;
549
+
550
+ const initialState = {
551
+ // @ts-expect-error testing private property
552
+ stripWidth: thumbnailStrip.stripWidth,
553
+ targetElement: thumbnailStrip.targetElement,
554
+ };
555
+
556
+ video.setAttribute("trimstart", "2s");
557
+ video.setAttribute("trimend", "2s");
558
+ await video.updateComplete;
559
+
560
+ // @ts-expect-error testing private property
561
+ expect(thumbnailStrip.stripWidth).toBe(initialState.stripWidth);
562
+ expect(thumbnailStrip.targetElement).toBe(initialState.targetElement);
563
+ expect(video.getAttribute("trimstart")).toBe("2s");
564
+ expect(video.getAttribute("trimend")).toBe("2s");
565
+ }, 1000);
566
+
567
+ test("should update dimensions when container resizes", async ({
568
+ expect,
569
+ alternateSetup,
570
+ }) => {
571
+ const { video, thumbnailStrip } = alternateSetup;
572
+
573
+ await video.mediaEngineTask.taskComplete;
574
+
575
+ // @ts-expect-error testing private property
576
+ const initialWidth = thumbnailStrip.stripWidth;
577
+ expect(initialWidth).toBe(400); // Container inner width
578
+
579
+ // Wait for any pending thumbnail layout tasks to complete
580
+ await awaitThumbnailLayout(thumbnailStrip);
581
+
582
+ // Simulate resize by directly setting the internal width and triggering update
583
+ (thumbnailStrip as any)._stripWidth = 800;
584
+ // @ts-expect-error testing private property
585
+ const finalWidth = thumbnailStrip.stripWidth;
586
+
587
+ expect(finalWidth).toBe(800);
588
+ expect(finalWidth).toBeGreaterThan(initialWidth);
589
+ }, 1000);
590
+ });
591
+ });