@editframe/elements 0.26.3-beta.0 โ†’ 0.26.4-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 (132) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-css.js +3 -3
  3. package/tsdown.config.ts +1 -1
  4. package/src/elements/ContextProxiesController.ts +0 -124
  5. package/src/elements/CrossUpdateController.ts +0 -22
  6. package/src/elements/EFAudio.browsertest.ts +0 -706
  7. package/src/elements/EFAudio.ts +0 -56
  8. package/src/elements/EFCaptions.browsertest.ts +0 -1960
  9. package/src/elements/EFCaptions.ts +0 -823
  10. package/src/elements/EFImage.browsertest.ts +0 -120
  11. package/src/elements/EFImage.ts +0 -113
  12. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
  13. package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
  14. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
  15. package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
  16. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
  17. package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
  18. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
  19. package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
  20. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
  21. package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
  22. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
  23. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
  24. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
  25. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
  26. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
  27. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
  28. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
  29. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
  30. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
  31. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
  32. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
  33. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
  34. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
  35. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
  36. package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
  37. package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
  38. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
  39. package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
  40. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
  41. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
  42. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
  43. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
  44. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
  45. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
  46. package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
  47. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
  48. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
  49. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
  50. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
  51. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
  52. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
  53. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
  54. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
  55. package/src/elements/EFMedia.browsertest.ts +0 -872
  56. package/src/elements/EFMedia.ts +0 -341
  57. package/src/elements/EFSourceMixin.ts +0 -60
  58. package/src/elements/EFSurface.browsertest.ts +0 -151
  59. package/src/elements/EFSurface.ts +0 -142
  60. package/src/elements/EFTemporal.browsertest.ts +0 -215
  61. package/src/elements/EFTemporal.ts +0 -800
  62. package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
  63. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
  64. package/src/elements/EFThumbnailStrip.ts +0 -906
  65. package/src/elements/EFTimegroup.browsertest.ts +0 -934
  66. package/src/elements/EFTimegroup.ts +0 -882
  67. package/src/elements/EFVideo.browsertest.ts +0 -1482
  68. package/src/elements/EFVideo.ts +0 -564
  69. package/src/elements/EFWaveform.ts +0 -547
  70. package/src/elements/FetchContext.browsertest.ts +0 -401
  71. package/src/elements/FetchMixin.ts +0 -38
  72. package/src/elements/SampleBuffer.ts +0 -94
  73. package/src/elements/TargetController.browsertest.ts +0 -230
  74. package/src/elements/TargetController.ts +0 -224
  75. package/src/elements/TimegroupController.ts +0 -26
  76. package/src/elements/durationConverter.ts +0 -35
  77. package/src/elements/parseTimeToMs.ts +0 -9
  78. package/src/elements/printTaskStatus.ts +0 -16
  79. package/src/elements/renderTemporalAudio.ts +0 -108
  80. package/src/elements/updateAnimations.browsertest.ts +0 -1884
  81. package/src/elements/updateAnimations.ts +0 -217
  82. package/src/elements/util.ts +0 -24
  83. package/src/gui/ContextMixin.browsertest.ts +0 -860
  84. package/src/gui/ContextMixin.ts +0 -562
  85. package/src/gui/Controllable.browsertest.ts +0 -258
  86. package/src/gui/Controllable.ts +0 -41
  87. package/src/gui/EFConfiguration.ts +0 -40
  88. package/src/gui/EFControls.browsertest.ts +0 -389
  89. package/src/gui/EFControls.ts +0 -195
  90. package/src/gui/EFDial.browsertest.ts +0 -84
  91. package/src/gui/EFDial.ts +0 -172
  92. package/src/gui/EFFilmstrip.browsertest.ts +0 -712
  93. package/src/gui/EFFilmstrip.ts +0 -1349
  94. package/src/gui/EFFitScale.ts +0 -152
  95. package/src/gui/EFFocusOverlay.ts +0 -79
  96. package/src/gui/EFPause.browsertest.ts +0 -202
  97. package/src/gui/EFPause.ts +0 -73
  98. package/src/gui/EFPlay.browsertest.ts +0 -202
  99. package/src/gui/EFPlay.ts +0 -73
  100. package/src/gui/EFPreview.ts +0 -74
  101. package/src/gui/EFResizableBox.browsertest.ts +0 -79
  102. package/src/gui/EFResizableBox.ts +0 -898
  103. package/src/gui/EFScrubber.ts +0 -151
  104. package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
  105. package/src/gui/EFTimeDisplay.ts +0 -55
  106. package/src/gui/EFToggleLoop.ts +0 -35
  107. package/src/gui/EFTogglePlay.ts +0 -70
  108. package/src/gui/EFWorkbench.ts +0 -115
  109. package/src/gui/PlaybackController.ts +0 -527
  110. package/src/gui/TWMixin.css +0 -6
  111. package/src/gui/TWMixin.ts +0 -61
  112. package/src/gui/TargetOrContextMixin.ts +0 -185
  113. package/src/gui/currentTimeContext.ts +0 -5
  114. package/src/gui/durationContext.ts +0 -3
  115. package/src/gui/efContext.ts +0 -6
  116. package/src/gui/fetchContext.ts +0 -5
  117. package/src/gui/focusContext.ts +0 -7
  118. package/src/gui/focusedElementContext.ts +0 -5
  119. package/src/gui/playingContext.ts +0 -5
  120. package/src/otel/BridgeSpanExporter.ts +0 -150
  121. package/src/otel/setupBrowserTracing.ts +0 -73
  122. package/src/otel/tracingHelpers.ts +0 -251
  123. package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
  124. package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
  125. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
  126. package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
  127. package/src/transcoding/types/index.ts +0 -312
  128. package/src/transcoding/utils/MediaUtils.ts +0 -63
  129. package/src/transcoding/utils/UrlGenerator.ts +0 -68
  130. package/src/transcoding/utils/constants.ts +0 -36
  131. package/src/utils/LRUCache.test.ts +0 -274
  132. package/src/utils/LRUCache.ts +0 -696
@@ -1,1482 +0,0 @@
1
- import { html, render } from "lit";
2
- import { beforeAll, beforeEach, describe, expect, vi } from "vitest";
3
-
4
- import { test as baseTest } from "../../test/useMSW.js";
5
- import type { EFVideo } from "./EFVideo.js";
6
- import "./EFVideo.js";
7
- import "../gui/EFWorkbench.js";
8
- import "../gui/EFPreview.js";
9
- import "./EFTimegroup.js";
10
-
11
- import type { EFTimegroup } from "./EFTimegroup.js";
12
-
13
- // Helper to wait for task completion but ignore abort errors
14
- async function waitForTaskIgnoringAborts(taskPromise: Promise<any>) {
15
- try {
16
- await taskPromise;
17
- } catch (error) {
18
- // Ignore AbortError - this is expected when tasks are cancelled due to new seeks
19
- if (error instanceof Error && error.name === "AbortError") {
20
- return;
21
- }
22
- throw error;
23
- }
24
- }
25
-
26
- beforeAll(async () => {
27
- console.clear();
28
- await fetch("/@ef-clear-cache", {
29
- method: "DELETE",
30
- });
31
- });
32
-
33
- beforeEach(() => {
34
- localStorage.clear();
35
- });
36
-
37
- // Extend the base test with fixtures following EFMedia.browsertest.ts pattern
38
- const test = baseTest.extend<{
39
- timegroup: EFTimegroup;
40
- configuration: any;
41
- headMoov480p: EFVideo;
42
- barsNtone: EFVideo;
43
- barsNtoneTimegroup: EFTimegroup;
44
- sequenceTimegroup: EFTimegroup;
45
- }>({
46
- timegroup: async ({}, use) => {
47
- const timegroup = document.createElement("ef-timegroup");
48
- timegroup.setAttribute("mode", "contain");
49
- await use(timegroup);
50
- },
51
- configuration: async ({}, use) => {
52
- const configuration = document.createElement("ef-configuration");
53
- const apiHost = "http://localhost:63315";
54
- configuration.setAttribute("api-host", apiHost);
55
- configuration.apiHost = apiHost;
56
- configuration.signingURL = ""; // Disable URL signing for tests
57
- document.body.appendChild(configuration);
58
- await use(configuration);
59
- },
60
- headMoov480p: async ({ configuration, timegroup }, use) => {
61
- localStorage.removeItem("ef-timegroup-root-this");
62
- const host = document.createElement("ef-video");
63
- host.src = "http://web:3000/head-moov-480p.mp4";
64
- timegroup.append(host);
65
- configuration.append(timegroup);
66
- await host.mediaEngineTask.run();
67
- await use(host);
68
- },
69
- barsNtone: async ({ barsNtoneTimegroup }, use) => {
70
- // The timegroup fixture will have already created the structure
71
- const video = barsNtoneTimegroup.querySelector("ef-video") as EFVideo;
72
- await video.updateComplete;
73
- use(video);
74
- },
75
- barsNtoneTimegroup: async ({}, use) => {
76
- // Clear localStorage to prevent test contamination
77
- localStorage.removeItem("ef-timegroup-barsNtoneTimegroup");
78
-
79
- const container = document.createElement("div");
80
- render(
81
- html`
82
- <ef-configuration api-host="http://localhost:63315" signing-url="">
83
- <ef-preview>
84
- <ef-timegroup mode="sequence" id="barsNtoneTimegroup"
85
- class="relative h-[500px] w-[1000px] overflow-hidden bg-slate-500">
86
- <ef-video src="bars-n-tone.mp4" id="barsNtoneVideo"></ef-video>
87
- </ef-timegroup>
88
- </ef-preview>
89
- </ef-configuration>
90
- `,
91
- container,
92
- );
93
- document.body.appendChild(container);
94
- const timegroup = container.querySelector("ef-timegroup") as EFTimegroup;
95
- await timegroup.updateComplete;
96
- await timegroup.waitForMediaDurations();
97
- await use(timegroup);
98
- // Cleanup: remove from DOM
99
- container.remove();
100
- },
101
- sequenceTimegroup: async ({}, use) => {
102
- const container = document.createElement("div");
103
- render(
104
- html`
105
- <ef-configuration api-host="http://localhost:63315" signing-url="">
106
- <ef-preview>
107
- <ef-timegroup mode="sequence"
108
- class="relative h-[500px] w-[1000px] overflow-hidden bg-slate-500">
109
-
110
- <ef-timegroup mode="contain" class="absolute w-full h-full">
111
- <ef-video src="bars-n-tone.mp4" class="size-full object-fit absolute top-0 left-0"></ef-video>
112
- </ef-timegroup>
113
-
114
- <ef-timegroup mode="contain" class="absolute w-full h-full">
115
- <ef-video src="bars-n-tone.mp4" class="size-full object-fit absolute top-0 left-0"></ef-video>
116
- </ef-timegroup>
117
-
118
- </ef-timegroup>
119
- </ef-preview>
120
- </ef-configuration>
121
- `,
122
- container,
123
- );
124
- document.body.appendChild(container);
125
- const timegroup = container.querySelector("ef-timegroup") as EFTimegroup;
126
- await timegroup.updateComplete;
127
- await timegroup.waitForMediaDurations();
128
- await use(timegroup);
129
- // Cleanup: remove from DOM
130
- container.remove();
131
- },
132
- });
133
-
134
- describe("EFVideo", () => {
135
- describe("basic rendering", () => {
136
- beforeEach(async () => {
137
- const response = await fetch("/@ef-clear-cache", {
138
- method: "DELETE",
139
- });
140
- await response.text();
141
- });
142
-
143
- test("should be defined and render canvas", async ({ expect }) => {
144
- const element = document.createElement("ef-video");
145
- document.body.appendChild(element);
146
-
147
- // Wait for element to render
148
- await element.updateComplete;
149
-
150
- expect(element.tagName).toBe("EF-VIDEO");
151
- expect(element.canvasElement).toBeDefined();
152
- expect(element.canvasElement?.tagName).toBe("CANVAS");
153
- });
154
-
155
- test("canvas has correct default properties", async ({ expect }) => {
156
- const container = document.createElement("div");
157
- render(html`<ef-video></ef-video>`, container);
158
- document.body.appendChild(container);
159
-
160
- const video = container.querySelector("ef-video") as EFVideo;
161
-
162
- // Wait for element to render
163
- await video.updateComplete;
164
-
165
- const canvas = video.canvasElement;
166
-
167
- expect(canvas).toBeDefined();
168
- expect(canvas?.width).toBeGreaterThan(0);
169
- expect(canvas?.height).toBeGreaterThan(0);
170
- });
171
-
172
- test("canvas inherits styling correctly", async ({ expect }) => {
173
- const container = document.createElement("div");
174
- render(
175
- html`
176
- <ef-video style="width: 640px; height: 360px;"></ef-video>
177
- `,
178
- container,
179
- );
180
- document.body.appendChild(container);
181
-
182
- const video = container.querySelector("ef-video") as EFVideo;
183
-
184
- // Wait for element to render
185
- await video.updateComplete;
186
-
187
- const canvas = video.canvasElement;
188
-
189
- expect(canvas).toBeDefined();
190
- // Canvas should inherit the styling
191
- const computedStyle = window.getComputedStyle(canvas!);
192
- expect(computedStyle.width).toBe("640px");
193
- expect(computedStyle.height).toBe("360px");
194
- });
195
- });
196
-
197
- describe("video asset integration", () => {
198
- test("integrates with video asset loading", async ({ expect }) => {
199
- const container = document.createElement("div");
200
- render(
201
- html`
202
- <ef-preview>
203
- <ef-video src="bars-n-tone.mp4" mode="asset"></ef-video>
204
- </ef-preview>
205
- `,
206
- container,
207
- );
208
- document.body.appendChild(container);
209
-
210
- const video = container.querySelector("ef-video") as EFVideo;
211
- await video.updateComplete;
212
-
213
- // Wait for media to be ready by waiting for the media engine task to complete
214
- await video.mediaEngineTask.taskComplete;
215
-
216
- expect(video.src).toBe("bars-n-tone.mp4");
217
-
218
- // The video should have loaded successfully and have a duration > 0
219
- expect(video.intrinsicDurationMs).toBeGreaterThan(0);
220
- });
221
-
222
- test("handles missing video asset gracefully", async ({ expect }) => {
223
- const container = document.createElement("div");
224
- render(
225
- html`
226
- <ef-preview>
227
- <ef-video src="/nonexistent.mp4"></ef-video>
228
- </ef-preview>
229
- `,
230
- container,
231
- );
232
- document.body.appendChild(container);
233
-
234
- const video = container.querySelector("ef-video") as EFVideo;
235
-
236
- // Should not throw when video asset is missing
237
- expect(() => {
238
- video.paint(0);
239
- }).not.toThrow();
240
- });
241
- });
242
-
243
- describe("frame painting and canvas updates", () => {
244
- test("canvas dimensions update when frame dimensions change", async ({
245
- expect,
246
- }) => {
247
- const container = document.createElement("div");
248
- render(html`<ef-video></ef-video>`, container);
249
- document.body.appendChild(container);
250
-
251
- const video = container.querySelector("ef-video") as EFVideo;
252
-
253
- // Wait for element to render
254
- await video.updateComplete;
255
-
256
- const canvas = video.canvasElement!;
257
-
258
- // Mock a video frame with specific dimensions
259
- const mockFrame = {
260
- codedWidth: 1920,
261
- codedHeight: 1080,
262
- format: "RGBA",
263
- timestamp: 0,
264
- close: vi.fn(),
265
- } as unknown as VideoFrame;
266
-
267
- // Simulate frame painting (this would normally happen through paint method)
268
- const ctx = canvas.getContext("2d");
269
- if (ctx && mockFrame.codedWidth && mockFrame.codedHeight) {
270
- canvas.width = mockFrame.codedWidth;
271
- canvas.height = mockFrame.codedHeight;
272
- // Mock drawing the frame
273
- ctx.fillStyle = "red";
274
- ctx.fillRect(0, 0, canvas.width, canvas.height);
275
- }
276
-
277
- expect(canvas.width).toBe(1920);
278
- expect(canvas.height).toBe(1080);
279
- });
280
-
281
- test("handles frame painting with null format gracefully", async ({
282
- expect,
283
- }) => {
284
- const container = document.createElement("div");
285
- render(html`<ef-video></ef-video>`, container);
286
- document.body.appendChild(container);
287
-
288
- const video = container.querySelector("ef-video") as EFVideo;
289
-
290
- // Wait for element to render
291
- await video.updateComplete;
292
-
293
- const canvas = video.canvasElement!;
294
-
295
- // Mock a frame with null format (edge case)
296
- const mockFrame = {
297
- codedWidth: 640,
298
- codedHeight: 480,
299
- format: null,
300
- timestamp: 0,
301
- close: vi.fn(),
302
- } as unknown as VideoFrame;
303
-
304
- const ctx = canvas.getContext("2d");
305
-
306
- // Should handle null format gracefully
307
- expect(() => {
308
- if (ctx && mockFrame.format === null) {
309
- console.warn("Frame format is null", mockFrame);
310
- return;
311
- }
312
- }).not.toThrow();
313
- });
314
-
315
- test("canvas context is available for drawing", async ({ expect }) => {
316
- const container = document.createElement("div");
317
- render(html`<ef-video></ef-video>`, container);
318
- document.body.appendChild(container);
319
-
320
- const video = container.querySelector("ef-video") as EFVideo;
321
-
322
- // Wait for element to render
323
- await video.updateComplete;
324
-
325
- const canvas = video.canvasElement!;
326
- const ctx = canvas.getContext("2d");
327
-
328
- expect(ctx).toBeDefined();
329
- expect(ctx).toBeInstanceOf(CanvasRenderingContext2D);
330
-
331
- // Test that we can draw on the canvas
332
- expect(() => {
333
- ctx!.fillStyle = "blue";
334
- ctx!.fillRect(0, 0, 100, 100);
335
- }).not.toThrow();
336
- });
337
- });
338
-
339
- describe("decoder lock scenarios", () => {
340
- test("handles concurrent paint attempts safely", async ({ expect }) => {
341
- const container = document.createElement("div");
342
- render(html`<ef-video></ef-video>`, container);
343
- document.body.appendChild(container);
344
-
345
- const video = container.querySelector("ef-video") as EFVideo;
346
-
347
- // Access the private decoder lock through reflection for testing
348
- const decoderLockDescriptor = Object.getOwnPropertyDescriptor(
349
- Object.getPrototypeOf(video),
350
- "#decoderLock",
351
- );
352
-
353
- // Simulate the decoder being in use
354
- if (decoderLockDescriptor) {
355
- // We can test that multiple paint calls don't cause issues
356
- expect(() => {
357
- video.paint(0);
358
- video.paint(0);
359
- video.paint(0);
360
- }).not.toThrow();
361
- }
362
- });
363
-
364
- test("paint handles missing canvas gracefully", ({ expect }) => {
365
- const container = document.createElement("div");
366
- render(html`<ef-video></ef-video>`, container);
367
- document.body.appendChild(container);
368
-
369
- const video = container.querySelector("ef-video") as EFVideo;
370
-
371
- // Remove canvas to test edge case
372
- const canvas = video.canvasElement;
373
- canvas?.remove();
374
-
375
- // Paint should handle missing canvas
376
- expect(() => video.paint(0)).not.toThrow();
377
- });
378
-
379
- test("handles paint with no video asset", ({ expect }) => {
380
- const container = document.createElement("div");
381
- render(html`<ef-video></ef-video>`, container);
382
- document.body.appendChild(container);
383
-
384
- const video = container.querySelector("ef-video") as EFVideo;
385
-
386
- // Paint should handle missing video asset gracefully
387
- expect(() => video.paint(0)).not.toThrow();
388
- });
389
- });
390
-
391
- describe("frame task integration", () => {
392
- test("frameTask coordinates all required tasks", async ({ expect }) => {
393
- const container = document.createElement("div");
394
- render(
395
- html`
396
- <ef-preview>
397
- <ef-video src="/test-video.mp4"></ef-video>
398
- </ef-preview>
399
- `,
400
- container,
401
- );
402
- document.body.appendChild(container);
403
-
404
- const video = container.querySelector("ef-video") as EFVideo;
405
-
406
- // frameTask should complete without errors even when other tasks fail
407
- expect(() => {
408
- video.frameTask.run();
409
- }).not.toThrow();
410
- });
411
-
412
- test("frameTask handles missing dependencies", ({ expect }) => {
413
- const container = document.createElement("div");
414
- render(html`<ef-video></ef-video>`, container);
415
- document.body.appendChild(container);
416
-
417
- const video = container.querySelector("ef-video") as EFVideo;
418
-
419
- // Should handle missing dependencies gracefully
420
- expect(() => {
421
- video.frameTask.run();
422
- }).not.toThrow();
423
- });
424
- });
425
-
426
- describe("error handling and edge cases", () => {
427
- test("handles seek to invalid time", ({ expect }) => {
428
- const container = document.createElement("div");
429
- render(html`<ef-video></ef-video>`, container);
430
- document.body.appendChild(container);
431
-
432
- const video = container.querySelector("ef-video") as EFVideo;
433
-
434
- // Should handle invalid seek times gracefully
435
- expect(() => {
436
- video.desiredSeekTimeMs = -1000; // Invalid negative time
437
- video.paint(-1000);
438
- }).not.toThrow();
439
-
440
- expect(() => {
441
- video.desiredSeekTimeMs = Number.POSITIVE_INFINITY;
442
- video.paint(Number.POSITIVE_INFINITY);
443
- }).not.toThrow();
444
- });
445
-
446
- test("handles video element removal during playback", ({ expect }) => {
447
- const container = document.createElement("div");
448
- render(html`<ef-video></ef-video>`, container);
449
- document.body.appendChild(container);
450
-
451
- const video = container.querySelector("ef-video") as EFVideo;
452
-
453
- // Start some operations
454
- video.paint(0);
455
-
456
- // Remove element
457
- video.remove();
458
-
459
- // Should not cause errors
460
- expect(() => {
461
- video.paint(0);
462
- }).not.toThrow();
463
- });
464
-
465
- test("handles canvas context loss gracefully", async ({ expect }) => {
466
- const container = document.createElement("div");
467
- render(html`<ef-video></ef-video>`, container);
468
- document.body.appendChild(container);
469
-
470
- const video = container.querySelector("ef-video") as EFVideo;
471
-
472
- // Wait for element to render
473
- await video.updateComplete;
474
-
475
- const canvas = video.canvasElement!;
476
-
477
- // Simulate context loss by making getContext return null
478
- const originalGetContext = canvas.getContext;
479
- canvas.getContext = vi.fn().mockReturnValue(null);
480
-
481
- // Should handle context loss gracefully
482
- expect(() => {
483
- video.paint(0);
484
- }).not.toThrow();
485
-
486
- // Restore original method
487
- canvas.getContext = originalGetContext;
488
- });
489
- });
490
-
491
- describe("assetId property", () => {
492
- test("reads assetId from html source", async ({ expect }) => {
493
- const container = document.createElement("div");
494
- container.innerHTML = `<ef-video asset-id="test-video-asset-123"></ef-video>`;
495
- document.body.appendChild(container);
496
-
497
- const video = container.querySelector("ef-video") as EFVideo;
498
- await video.updateComplete;
499
-
500
- expect(video).toBeDefined();
501
- expect(video.assetId).toBe("test-video-asset-123");
502
-
503
- container.remove();
504
- });
505
-
506
- test("reads from js property", ({ expect }) => {
507
- const container = document.createElement("div");
508
- render(html`<ef-video></ef-video>`, container);
509
- const video = container.querySelector("ef-video") as EFVideo;
510
-
511
- video.assetId = "test-video-456";
512
- expect(video.assetId).toBe("test-video-456");
513
- });
514
-
515
- test("reflects property changes to attribute", async ({ expect }) => {
516
- const container = document.createElement("div");
517
- render(html`<ef-video></ef-video>`, container);
518
- document.body.appendChild(container);
519
-
520
- const video = container.querySelector("ef-video") as EFVideo;
521
- await video.updateComplete;
522
-
523
- video.assetId = "test-video-789";
524
- await video.updateComplete;
525
- expect(video.getAttribute("asset-id")).toBe("test-video-789");
526
-
527
- video.assetId = null;
528
- await video.updateComplete;
529
- expect(video.hasAttribute("asset-id")).toBe(false);
530
-
531
- container.remove();
532
- });
533
- });
534
-
535
- describe("integration with timegroups", () => {
536
- test("integrates correctly within timegroup structure", async ({
537
- expect,
538
- }) => {
539
- const container = document.createElement("div");
540
- render(
541
- html`
542
- <ef-preview>
543
- <ef-timegroup mode="sequence">
544
- <ef-video src="bars-n-tone.mp4" mode="asset"></ef-video>
545
- </ef-timegroup>
546
- </ef-preview>
547
- `,
548
- container,
549
- );
550
- document.body.appendChild(container);
551
-
552
- const video = container.querySelector("ef-video") as EFVideo;
553
- const timegroup = container.querySelector("ef-timegroup");
554
- await video.updateComplete;
555
-
556
- // Wait for media to be ready by waiting for the media engine task to complete
557
- await video.mediaEngineTask.taskComplete;
558
-
559
- expect(timegroup).toBeDefined();
560
-
561
- // The video should have loaded successfully within the timegroup
562
- expect(video.intrinsicDurationMs).toBeGreaterThan(0);
563
- });
564
-
565
- test("works as standalone root temporal in ef-preview", async ({
566
- expect,
567
- }) => {
568
- const container = document.createElement("div");
569
- render(
570
- html`
571
- <ef-configuration api-host="http://localhost:63315" signing-url="">
572
- <ef-preview id="test-preview">
573
- <ef-video src="bars-n-tone.mp4" mode="asset" id="standalone-video"></ef-video>
574
- </ef-preview>
575
- </ef-configuration>
576
- `,
577
- container,
578
- );
579
- document.body.appendChild(container);
580
-
581
- const preview = container.querySelector("ef-preview") as any;
582
- const video = container.querySelector("ef-video") as EFVideo;
583
-
584
- await preview.updateComplete;
585
- await video.updateComplete;
586
-
587
- // Wait for media to be ready
588
- await video.mediaEngineTask.taskComplete;
589
-
590
- // Video should have loaded successfully
591
- expect(video.intrinsicDurationMs).toBeGreaterThan(0);
592
-
593
- // Preview should recognize the video as its root temporal
594
- expect(preview.targetTemporal).toBe(video);
595
-
596
- // Video should have a playback controller as a root element
597
- expect(video.playbackController).toBeDefined();
598
-
599
- // Preview should be able to control playback
600
- expect(preview.playing).toBe(false);
601
-
602
- // Seek the video through the preview
603
- preview.currentTimeMs = 1000;
604
- await video.frameTask.taskComplete;
605
-
606
- // Video should have seeked
607
- expect(video.ownCurrentTimeMs).toBeCloseTo(1000, 0);
608
-
609
- // Cleanup
610
- container.remove();
611
- });
612
- });
613
-
614
- describe.skip("loading indicator", () => {
615
- test("should not show loading indicator for operations completing under 250ms", async ({
616
- expect,
617
- }) => {
618
- const container = document.createElement("div");
619
- render(html`<ef-video></ef-video>`, container);
620
- document.body.appendChild(container);
621
-
622
- const video = container.querySelector("ef-video") as EFVideo;
623
- await video.updateComplete;
624
-
625
- // Start a fast operation
626
- video.startDelayedLoading("test-fast", "Fast operation");
627
-
628
- // Clear it quickly (under 250ms)
629
- setTimeout(() => {
630
- video.clearDelayedLoading("test-fast");
631
- }, 100);
632
-
633
- expect(video.loadingState.isLoading).toBe(false);
634
- });
635
-
636
- test("should show loading indicator only after 250ms for slow operations", async ({
637
- expect,
638
- }) => {
639
- const container = document.createElement("div");
640
- render(html`<ef-video></ef-video>`, container);
641
- document.body.appendChild(container);
642
-
643
- const video = container.querySelector("ef-video") as EFVideo;
644
- await video.updateComplete;
645
-
646
- // Start a slow operation
647
- video.startDelayedLoading("test-slow", "Slow operation");
648
-
649
- // Should not be loading immediately
650
- expect(video.loadingState.isLoading).toBe(false);
651
-
652
- // Should now be loading
653
- expect(video.loadingState.isLoading).toBe(true);
654
- expect(video.loadingState.message).toBe("Slow operation");
655
-
656
- // Clear the loading
657
- video.clearDelayedLoading("test-slow");
658
-
659
- // Should stop loading
660
- expect(video.loadingState.isLoading).toBe(false);
661
- });
662
-
663
- test("should handle multiple concurrent loading operations", async ({
664
- expect,
665
- }) => {
666
- const container = document.createElement("div");
667
- render(html`<ef-video></ef-video>`, container);
668
- document.body.appendChild(container);
669
-
670
- const video = container.querySelector("ef-video") as EFVideo;
671
- await video.updateComplete;
672
-
673
- // Start multiple operations
674
- video.startDelayedLoading("op1", "Operation 1");
675
- video.startDelayedLoading("op2", "Operation 2");
676
-
677
- // Should be loading
678
- expect(video.loadingState.isLoading).toBe(true);
679
-
680
- // Clear one operation
681
- video.clearDelayedLoading("op1");
682
-
683
- // Should still be loading (op2 still active)
684
- expect(video.loadingState.isLoading).toBe(true);
685
-
686
- // Clear second operation
687
- video.clearDelayedLoading("op2");
688
-
689
- // Should stop loading
690
- expect(video.loadingState.isLoading).toBe(false);
691
- });
692
-
693
- test("should not show loading for background operations", async ({
694
- expect,
695
- }) => {
696
- const container = document.createElement("div");
697
- render(html`<ef-video></ef-video>`, container);
698
- document.body.appendChild(container);
699
-
700
- const video = container.querySelector("ef-video") as EFVideo;
701
- await video.updateComplete;
702
-
703
- // Start a background operation
704
- video.startDelayedLoading("bg-op", "Background operation", {
705
- background: true,
706
- });
707
-
708
- // Should not show loading UI for background operations
709
- expect(video.loadingState.isLoading).toBe(false);
710
-
711
- // Clear the operation
712
- video.clearDelayedLoading("bg-op");
713
- });
714
-
715
- test("should properly clean up loading state on disconnect", async ({
716
- expect,
717
- }) => {
718
- const container = document.createElement("div");
719
- render(html`<ef-video></ef-video>`, container);
720
- document.body.appendChild(container);
721
-
722
- const video = container.querySelector("ef-video") as EFVideo;
723
- await video.updateComplete;
724
-
725
- // Start an operation
726
- video.startDelayedLoading("cleanup-test", "Test operation");
727
-
728
- // Disconnect the element
729
- video.remove();
730
-
731
- // Loading should be cleared
732
- expect(video.loadingState.isLoading).toBe(false);
733
- });
734
- });
735
-
736
- describe("AssetMediaEngine", () => {
737
- test("seeks to 8074ms", async ({ barsNtone, barsNtoneTimegroup }) => {
738
- // Wait for any initial loading to complete
739
- await waitForTaskIgnoringAborts(
740
- barsNtone.unifiedVideoSeekTask.taskComplete,
741
- );
742
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
743
-
744
- // Use timegroup for seeking to ensure audio and video are synchronized
745
- barsNtoneTimegroup.currentTimeMs = 8074;
746
- await barsNtone.updateComplete;
747
-
748
- // Wait for the new seek tasks to complete (ignoring any aborts from previous operations)
749
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
750
- await waitForTaskIgnoringAborts(
751
- barsNtone.unifiedVideoSeekTask.taskComplete,
752
- );
753
- });
754
-
755
- test("seeks to beginning of video (0ms)", async ({
756
- barsNtone,
757
- barsNtoneTimegroup,
758
- }) => {
759
- await waitForTaskIgnoringAborts(
760
- barsNtone.unifiedVideoSeekTask.taskComplete,
761
- );
762
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
763
- barsNtoneTimegroup.currentTimeMs = 0;
764
- await barsNtone.updateComplete;
765
-
766
- // Wait for the new seek tasks to complete
767
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
768
- await waitForTaskIgnoringAborts(
769
- barsNtone.unifiedVideoSeekTask.taskComplete,
770
- );
771
- });
772
-
773
- test("seeks to exact segment boundary at 2066ms", async ({
774
- barsNtone,
775
- barsNtoneTimegroup,
776
- }) => {
777
- await waitForTaskIgnoringAborts(
778
- barsNtone.unifiedVideoSeekTask.taskComplete,
779
- );
780
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
781
- // This is approximately where segment 0 ends and segment 1 begins
782
- barsNtoneTimegroup.currentTimeMs = 2066;
783
- await barsNtone.updateComplete;
784
-
785
- // Wait for the new seek tasks to complete
786
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
787
- await waitForTaskIgnoringAborts(
788
- barsNtone.unifiedVideoSeekTask.taskComplete,
789
- );
790
- });
791
-
792
- test("seeks to exact segment boundary at 4033ms", async ({
793
- barsNtone,
794
- barsNtoneTimegroup,
795
- }) => {
796
- await waitForTaskIgnoringAborts(
797
- barsNtone.unifiedVideoSeekTask.taskComplete,
798
- );
799
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
800
- // This is approximately where segment 1 ends and segment 2 begins
801
- barsNtoneTimegroup.currentTimeMs = 4033;
802
- await barsNtone.updateComplete;
803
-
804
- // Wait for the new seek tasks to complete
805
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
806
- await waitForTaskIgnoringAborts(
807
- barsNtone.unifiedVideoSeekTask.taskComplete,
808
- );
809
- });
810
-
811
- test("seeks to exact segment boundary at 6066ms", async ({
812
- barsNtone,
813
- barsNtoneTimegroup,
814
- }) => {
815
- await waitForTaskIgnoringAborts(
816
- barsNtone.unifiedVideoSeekTask.taskComplete,
817
- );
818
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
819
- // Reset to 0 first to ensure clean state
820
- barsNtoneTimegroup.currentTimeMs = 0;
821
- await barsNtone.updateComplete;
822
- // Wait for both audio and video to complete the reset
823
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
824
- await waitForTaskIgnoringAborts(
825
- barsNtone.unifiedVideoSeekTask.taskComplete,
826
- );
827
-
828
- // Updated: Use time safely within segment boundaries (6000ms instead of 6066ms)
829
- // The actual boundary is at 6066.67ms, so 6000ms should be in segment 2
830
- barsNtoneTimegroup.currentTimeMs = 6000;
831
- await barsNtone.updateComplete;
832
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
833
- await waitForTaskIgnoringAborts(
834
- barsNtone.unifiedVideoSeekTask.taskComplete,
835
- );
836
- });
837
-
838
- test("seeks to exact segment boundary at 8033ms", async ({
839
- barsNtone,
840
- barsNtoneTimegroup,
841
- }) => {
842
- await waitForTaskIgnoringAborts(
843
- barsNtone.unifiedVideoSeekTask.taskComplete,
844
- );
845
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
846
- // This is approximately where segment 2 ends and segment 3 begins
847
- barsNtoneTimegroup.currentTimeMs = 8033;
848
- await barsNtone.updateComplete;
849
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
850
- await waitForTaskIgnoringAborts(
851
- barsNtone.unifiedVideoSeekTask.taskComplete,
852
- );
853
- });
854
-
855
- test("seeks to near end of video at 9900ms", async ({
856
- barsNtone,
857
- barsNtoneTimegroup,
858
- }) => {
859
- await waitForTaskIgnoringAborts(
860
- barsNtone.unifiedVideoSeekTask.taskComplete,
861
- );
862
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
863
- // Seek to near the end of the video
864
- barsNtoneTimegroup.currentTimeMs = 9900;
865
- await barsNtone.updateComplete;
866
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
867
- await waitForTaskIgnoringAborts(
868
- barsNtone.unifiedVideoSeekTask.taskComplete,
869
- );
870
- });
871
-
872
- test("seeks backward from 8000ms to 2000ms", async ({
873
- barsNtone,
874
- barsNtoneTimegroup,
875
- expect,
876
- }) => {
877
- await barsNtoneTimegroup.seek(8000);
878
- expect(barsNtone.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
879
- 8.066,
880
- );
881
-
882
- // Then seek backward
883
- await barsNtoneTimegroup.seek(2000);
884
- expect(barsNtone.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
885
- 2.066,
886
- );
887
- });
888
-
889
- test("seeks to multiple points across segments", async ({
890
- barsNtone,
891
- barsNtoneTimegroup,
892
- }) => {
893
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
894
-
895
- // Use seek points that are within the actual media duration
896
- // Based on the fragment index, the media is about 9.8 seconds long
897
- const seekPoints = [1000, 3000, 5000, 7000, 9000];
898
-
899
- for (const seekPoint of seekPoints) {
900
- barsNtoneTimegroup.currentTimeMs = seekPoint;
901
- await barsNtone.updateComplete;
902
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
903
- await waitForTaskIgnoringAborts(
904
- barsNtone.unifiedVideoSeekTask.taskComplete,
905
- );
906
- }
907
- });
908
-
909
- test("seeks just before segment boundary at 8030ms", async ({
910
- barsNtone,
911
- barsNtoneTimegroup,
912
- }) => {
913
- await waitForTaskIgnoringAborts(
914
- barsNtone.unifiedVideoSeekTask.taskComplete,
915
- );
916
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
917
- // Use a safe seek time within the media duration
918
- barsNtoneTimegroup.currentTimeMs = 8030;
919
- await barsNtone.updateComplete;
920
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
921
- await waitForTaskIgnoringAborts(
922
- barsNtone.unifiedVideoSeekTask.taskComplete,
923
- );
924
- });
925
-
926
- test("seeks just after segment boundary at 8070ms", async ({
927
- barsNtone,
928
- barsNtoneTimegroup,
929
- }) => {
930
- await waitForTaskIgnoringAborts(
931
- barsNtone.unifiedVideoSeekTask.taskComplete,
932
- );
933
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
934
- // Use a safe seek time within the media duration
935
- barsNtoneTimegroup.currentTimeMs = 8070;
936
- await barsNtone.updateComplete;
937
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
938
- await waitForTaskIgnoringAborts(
939
- barsNtone.unifiedVideoSeekTask.taskComplete,
940
- );
941
- });
942
-
943
- test("handles rapid scrubbing between segments", async ({
944
- barsNtone,
945
- barsNtoneTimegroup,
946
- }) => {
947
- await waitForTaskIgnoringAborts(
948
- barsNtone.unifiedVideoSeekTask.taskComplete,
949
- );
950
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
951
-
952
- // Simulate rapid scrubbing back and forth across segments
953
- // Use times that are within the actual media duration
954
- const scrubSequence = [
955
- 0, // Start
956
- 1000, // Jump to segment 1
957
- 3000, // Back to segment 0
958
- 5000, // Forward to segment 2
959
- 7000, // Back to segment 1
960
- 9000, // Forward to segment 3
961
- 0, // Back to segment 0
962
- 1000, // Jump to segment 1
963
- ];
964
-
965
- for (const timeMs of scrubSequence) {
966
- // Don't wait for completion between rapid scrubs to simulate the race condition
967
- barsNtoneTimegroup.currentTimeMs = timeMs;
968
- await barsNtone.updateComplete;
969
- }
970
-
971
- // Final seek operations should complete without errors
972
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
973
- await waitForTaskIgnoringAborts(
974
- barsNtone.unifiedVideoSeekTask.taskComplete,
975
- );
976
- });
977
-
978
- test("handles concurrent seeks to different segments", async ({
979
- expect,
980
- barsNtone,
981
- barsNtoneTimegroup,
982
- }) => {
983
- await waitForTaskIgnoringAborts(
984
- barsNtone.unifiedVideoSeekTask.taskComplete,
985
- );
986
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
987
-
988
- // Start multiple seeks without waiting for completion
989
- const seekPromises = [];
990
-
991
- // Seek to beginning of segment 0
992
- barsNtoneTimegroup.currentTimeMs = 100;
993
- await barsNtone.updateComplete;
994
- seekPromises.push(
995
- Promise.allSettled([
996
- barsNtone.audioSeekTask.taskComplete,
997
- barsNtone.unifiedVideoSeekTask.taskComplete,
998
- ]),
999
- );
1000
-
1001
- // Immediately seek to middle of video (different segment)
1002
- barsNtoneTimegroup.currentTimeMs = 5000;
1003
- await barsNtone.updateComplete;
1004
- seekPromises.push(
1005
- Promise.allSettled([
1006
- barsNtone.audioSeekTask.taskComplete,
1007
- barsNtone.unifiedVideoSeekTask.taskComplete,
1008
- ]),
1009
- );
1010
-
1011
- // Immediately seek to end (within valid range)
1012
- barsNtoneTimegroup.currentTimeMs = 9000;
1013
- await barsNtone.updateComplete;
1014
- seekPromises.push(
1015
- Promise.allSettled([
1016
- barsNtone.audioSeekTask.taskComplete,
1017
- barsNtone.unifiedVideoSeekTask.taskComplete,
1018
- ]),
1019
- );
1020
-
1021
- // Wait for all seeks to complete
1022
- const results = await Promise.all(seekPromises);
1023
-
1024
- // At least the final seek should succeed
1025
- const finalResults = results[results.length - 1];
1026
- expect(finalResults).toBeDefined();
1027
- expect(finalResults?.some((r) => r.status === "fulfilled")).toBe(true);
1028
- });
1029
-
1030
- test("recovers from segment range errors during scrubbing", async ({
1031
- barsNtone,
1032
- barsNtoneTimegroup,
1033
- }) => {
1034
- await waitForTaskIgnoringAborts(
1035
- barsNtone.unifiedVideoSeekTask.taskComplete,
1036
- );
1037
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
1038
-
1039
- // Try to reproduce the exact error scenario
1040
- // First seek to segment 0
1041
- barsNtoneTimegroup.currentTimeMs = 1000;
1042
- await barsNtone.updateComplete;
1043
-
1044
- // Then immediately seek to a time that would be in segment 2
1045
- // This might cause the range error if segment 0 is still loaded
1046
- barsNtoneTimegroup.currentTimeMs = 5000; // Safe time within media duration
1047
- await barsNtone.updateComplete;
1048
-
1049
- // The system should recover and eventually succeed
1050
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
1051
- await waitForTaskIgnoringAborts(
1052
- barsNtone.unifiedVideoSeekTask.taskComplete,
1053
- );
1054
- });
1055
-
1056
- test("seeks to 7975ms", async ({
1057
- barsNtone,
1058
- barsNtoneTimegroup,
1059
- expect,
1060
- }) => {
1061
- await barsNtoneTimegroup.seek(7975);
1062
-
1063
- expect(barsNtone.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
1064
- 8.033,
1065
- );
1066
- });
1067
-
1068
- test("seeks to 8041.667ms in video track 1", async ({
1069
- barsNtone,
1070
- barsNtoneTimegroup,
1071
- expect,
1072
- }) => {
1073
- await barsNtoneTimegroup.seekTask.taskComplete;
1074
- await barsNtoneTimegroup.seek(8041.667);
1075
- expect(barsNtone.unifiedVideoSeekTask.value?.timestamp).toBe(8.1);
1076
- });
1077
-
1078
- test("seeks to 10000ms near end of file", async ({
1079
- barsNtone,
1080
- barsNtoneTimegroup,
1081
- }) => {
1082
- await waitForTaskIgnoringAborts(
1083
- barsNtone.unifiedVideoSeekTask.taskComplete,
1084
- );
1085
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
1086
-
1087
- // Use a safe seek time within the media duration
1088
- // The original test was trying to seek to 10000ms which is outside the valid range
1089
- barsNtoneTimegroup.currentTimeMs = 10000;
1090
- await barsNtone.updateComplete;
1091
-
1092
- // Should not throw "Sample not found" errors
1093
- await waitForTaskIgnoringAborts(
1094
- barsNtone.unifiedVideoSeekTask.taskComplete,
1095
- );
1096
- await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
1097
- });
1098
- });
1099
-
1100
- describe("JIT Transcoder", () => {
1101
- test("seeks to start at 0ms", async ({
1102
- timegroup,
1103
- headMoov480p,
1104
- expect,
1105
- }) => {
1106
- await waitForTaskIgnoringAborts(
1107
- headMoov480p.unifiedVideoSeekTask.taskComplete,
1108
- );
1109
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1110
-
1111
- timegroup.currentTimeMs = 0;
1112
- await timegroup.seekTask.taskComplete;
1113
-
1114
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(0);
1115
- });
1116
-
1117
- test("seeks to 1000ms", async ({ timegroup, headMoov480p, expect }) => {
1118
- await timegroup.seek(1000);
1119
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(1);
1120
- });
1121
-
1122
- test("seeks to 3000ms", async ({ timegroup, headMoov480p, expect }) => {
1123
- await timegroup.seek(3000);
1124
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(3);
1125
- });
1126
-
1127
- test("seeks to 5000ms", async ({ timegroup, headMoov480p, expect }) => {
1128
- await timegroup.seek(5000);
1129
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(5);
1130
- });
1131
-
1132
- test("seeks to 7500ms", async ({ timegroup, headMoov480p, expect }) => {
1133
- await timegroup.seek(7500);
1134
-
1135
- // JIT transcoding returns actual video frame timestamps, not idealized segment boundaries
1136
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
1137
- 7.5,
1138
- 1,
1139
- );
1140
- });
1141
-
1142
- test("seeks to 8500ms", async ({ timegroup, headMoov480p, expect }) => {
1143
- await timegroup.seek(8500);
1144
-
1145
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
1146
- 8.5,
1147
- 1,
1148
- );
1149
- });
1150
-
1151
- test("seeks to near end at 9000ms", async ({
1152
- timegroup,
1153
- headMoov480p,
1154
- expect,
1155
- }) => {
1156
- await timegroup.seek(9000);
1157
-
1158
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(9);
1159
- });
1160
-
1161
- test("seeks backward from 7000ms to 2000ms", async ({
1162
- timegroup,
1163
- headMoov480p,
1164
- expect,
1165
- }) => {
1166
- await timegroup.seek(7000);
1167
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(7);
1168
-
1169
- await timegroup.seek(2000);
1170
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(2);
1171
- });
1172
-
1173
- test("seeks to multiple points in sequence", async ({
1174
- timegroup,
1175
- headMoov480p,
1176
- expect,
1177
- }) => {
1178
- const seekPoints = [1000, 3000, 5000, 2000, 6000, 0];
1179
- const expectedTimestamps = [1, 3, 5, 2, 6, 0];
1180
-
1181
- for (let i = 0; i < seekPoints.length; i++) {
1182
- await timegroup.seek(seekPoints[i]!);
1183
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
1184
- expectedTimestamps[i]!,
1185
- 1,
1186
- );
1187
- }
1188
- });
1189
-
1190
- test("seeks to fractional timestamps", async ({
1191
- timegroup,
1192
- headMoov480p,
1193
- expect,
1194
- }) => {
1195
- const fractionalTimes = [1234.567, 3456.789, 5678.901];
1196
- const expectedTimestamps = [1.234567, 3.456789, 5.678901];
1197
-
1198
- for (let i = 0; i < fractionalTimes.length; i++) {
1199
- await timegroup.seek(fractionalTimes[i]!);
1200
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
1201
- expectedTimestamps[i]!,
1202
- 1,
1203
- );
1204
- }
1205
- });
1206
-
1207
- test("frame tasks are not complete until internal video seek is complete", async ({
1208
- timegroup,
1209
- headMoov480p,
1210
- expect,
1211
- }) => {
1212
- await timegroup.seek(0);
1213
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
1214
- 0,
1215
- 1,
1216
- );
1217
-
1218
- await timegroup.seek(1000);
1219
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
1220
- 1,
1221
- 1,
1222
- );
1223
-
1224
- await timegroup.seek(4000);
1225
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
1226
- 4,
1227
- 1,
1228
- );
1229
- });
1230
-
1231
- test("rapid succession seeks cause intermediate seeks to be skipped", async ({
1232
- timegroup,
1233
- headMoov480p,
1234
- expect,
1235
- }) => {
1236
- await timegroup.seek(1000);
1237
- expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(1);
1238
-
1239
- // // Track frameTask executions using a spy on the run method
1240
- // const runSpy = vi.spyOn(timegroup.frameTask, 'run');
1241
-
1242
- // // Rapid succession of seeks - intermediate ones should be skipped
1243
- // timegroup.currentTimeMs = 1000;
1244
- // timegroup.currentTimeMs = 2000;
1245
- // timegroup.currentTimeMs = 3000;
1246
- // timegroup.currentTimeMs = 4000;
1247
- // timegroup.currentTimeMs = 1000;
1248
- // timegroup.currentTimeMs = 2000;
1249
- // timegroup.currentTimeMs = 3000;
1250
- // timegroup.currentTimeMs = 8000;
1251
-
1252
- // await timegroup.seekTask.taskComplete;
1253
- // expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(8);
1254
- // expect(runSpy).toHaveBeenCalledTimes(3);
1255
- });
1256
- });
1257
-
1258
- describe("audio analysis tasks with timeline sequences", () => {
1259
- test("should handle audio analysis when seeking into second video in sequence", async ({
1260
- sequenceTimegroup,
1261
- }) => {
1262
- // Use the sequence fixture which creates two videos in sequence
1263
- const videos = sequenceTimegroup.querySelectorAll(
1264
- "ef-video",
1265
- ) as NodeListOf<EFVideo>;
1266
- const video1 = videos[0]!;
1267
- const video2 = videos[1]!;
1268
-
1269
- // Wait for initial loading
1270
- await waitForTaskIgnoringAborts(video1.unifiedVideoSeekTask.taskComplete);
1271
- await waitForTaskIgnoringAborts(video1.audioSeekTask.taskComplete);
1272
- await waitForTaskIgnoringAborts(video2.unifiedVideoSeekTask.taskComplete);
1273
- await waitForTaskIgnoringAborts(video2.audioSeekTask.taskComplete);
1274
-
1275
- // Get the duration of the first video to know where the second video starts
1276
- const firstVideoDuration = video1.intrinsicDurationMs || 10000;
1277
-
1278
- // Seek into the second video (after the first one ends)
1279
- const secondVideoSeekTime = firstVideoDuration + 1000;
1280
- sequenceTimegroup.currentTimeMs = secondVideoSeekTime;
1281
- await sequenceTimegroup.updateComplete;
1282
-
1283
- // Both videos should handle the timeline positioning correctly
1284
- await waitForTaskIgnoringAborts(video1.audioSeekTask.taskComplete);
1285
- await waitForTaskIgnoringAborts(video2.audioSeekTask.taskComplete);
1286
- });
1287
-
1288
- test("fixed: JIT transcoding off-by-one bug for exact duration seeks", async ({
1289
- headMoov480p, // This uses JIT transcoding, not asset transcoding
1290
- }) => {
1291
- // This test verifies the fix for the off-by-one bug in JitMediaEngine.computeSegmentId
1292
- const timegroup = headMoov480p.closest("ef-timegroup") as EFTimegroup;
1293
-
1294
- // Wait for initial loading to complete
1295
- await waitForTaskIgnoringAborts(
1296
- headMoov480p.unifiedVideoSeekTask.taskComplete,
1297
- );
1298
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1299
-
1300
- // The fix: JitMediaEngine.computeSegmentId should handle seeking to exact duration
1301
- // Before fix: if (desiredSeekTimeMs >= this.durationMs) { return undefined; } โŒ
1302
- // After fix: if (desiredSeekTimeMs > this.durationMs) { return undefined; } โœ…
1303
-
1304
- // Get the media engine to verify it's JIT transcoding
1305
- const mediaEngine = headMoov480p.mediaEngineTask.value;
1306
-
1307
- if (mediaEngine?.constructor.name === "JitMediaEngine") {
1308
- // Test seeking to exact duration - this should NOT fail with "Segment ID is not available"
1309
- const exactDuration = headMoov480p.intrinsicDurationMs;
1310
-
1311
- timegroup.currentTimeMs = exactDuration;
1312
- await headMoov480p.updateComplete;
1313
-
1314
- // This should now work without throwing "Segment ID is not available"
1315
- await waitForTaskIgnoringAborts(
1316
- headMoov480p.unifiedVideoSeekTask.taskComplete,
1317
- );
1318
- await waitForTaskIgnoringAborts(
1319
- headMoov480p.audioSeekTask.taskComplete,
1320
- );
1321
- }
1322
- });
1323
-
1324
- test("FIXED: audio analysis tasks handle out-of-bounds time ranges gracefully", async ({
1325
- headMoov480p,
1326
- }) => {
1327
- // This test verifies the fix for "No segments found for time range 10000-15000ms" error
1328
- const timegroup = headMoov480p.closest("ef-timegroup") as EFTimegroup;
1329
-
1330
- // Wait for initial loading to complete
1331
- await waitForTaskIgnoringAborts(
1332
- headMoov480p.unifiedVideoSeekTask.taskComplete,
1333
- );
1334
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1335
-
1336
- console.log("๐Ÿงช TESTING: Audio analysis out-of-bounds time range fix");
1337
- console.log(`๐Ÿ“Š Video duration: ${headMoov480p.intrinsicDurationMs}ms`);
1338
-
1339
- // Seek to exactly the end of the video to trigger the audio analysis tasks
1340
- const exactDuration = headMoov480p.intrinsicDurationMs; // Should be 10000ms
1341
- timegroup.currentTimeMs = exactDuration;
1342
- await headMoov480p.updateComplete;
1343
-
1344
- // The fix: audio analysis tasks should now clamp their time ranges to video duration
1345
- // Before fix: requested "10000-15000ms" โ†’ "No segments found" error
1346
- // After fix: requested "10000-10000ms" โ†’ gracefully skipped or clamped to available range
1347
-
1348
- console.log(
1349
- `๐ŸŽฏ EXPECTED FIX: Audio analysis tasks should clamp range to ${exactDuration}-${exactDuration}ms`,
1350
- );
1351
- console.log("๐ŸŽฏ Or gracefully skip analysis when seeking beyond end");
1352
-
1353
- // Let the audio analysis tasks run - they should now handle this gracefully
1354
-
1355
- // The basic seek should complete without errors
1356
- await waitForTaskIgnoringAborts(
1357
- headMoov480p.unifiedVideoSeekTask.taskComplete,
1358
- );
1359
-
1360
- // Audio tasks may still throw their own errors, but not the "No segments found" error
1361
- // We don't explicitly test the audio analysis tasks here since they might legitimately
1362
- // return null when seeking beyond the end, which is the expected behavior
1363
- });
1364
-
1365
- test("FIXED: rapid seeking race condition handled gracefully", async ({
1366
- expect,
1367
- headMoov480p,
1368
- }) => {
1369
- // This test verifies the fix for race condition where rapid seeks cause
1370
- // "Seek time Xms is before track start Yms" errors
1371
- const timegroup = headMoov480p.closest("ef-timegroup") as EFTimegroup;
1372
-
1373
- // Wait for initial loading to complete
1374
- await waitForTaskIgnoringAborts(
1375
- headMoov480p.unifiedVideoSeekTask.taskComplete,
1376
- );
1377
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1378
-
1379
- console.log("๐Ÿงช TESTING: Rapid seeking race condition fix");
1380
- console.log(`๐Ÿ“Š Video duration: ${headMoov480p.intrinsicDurationMs}ms`);
1381
-
1382
- // Simulate rapid seeking that previously caused race conditions
1383
- // Now should be handled gracefully with warnings instead of errors
1384
- const rapidSeekSequence = [
1385
- 2000, // Start at 2s
1386
- 7000, // Jump to 7s
1387
- 1000, // Back to 1s (previously caused race condition)
1388
- 8000, // Jump to 8s
1389
- 500, // Back to 0.5s (previously caused race condition)
1390
- 5000, // Jump to 5s
1391
- ];
1392
-
1393
- for (const seekTime of rapidSeekSequence) {
1394
- timegroup.currentTimeMs = seekTime;
1395
- await headMoov480p.updateComplete;
1396
- }
1397
-
1398
- // The fix should prevent errors - both video and audio tasks should complete
1399
- await waitForTaskIgnoringAborts(
1400
- headMoov480p.unifiedVideoSeekTask.taskComplete,
1401
- );
1402
-
1403
- // Audio tasks should also complete without throwing, though they may log warnings
1404
- await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
1405
-
1406
- // Test passes if we reach here without unhandled errors
1407
- expect(true).toBe(true);
1408
- });
1409
- });
1410
-
1411
- describe("loop attribute", () => {
1412
- test(
1413
- "standalone ef-video respects loop attribute",
1414
- { timeout: 1000 },
1415
- async () => {
1416
- const container = document.createElement("div");
1417
- render(
1418
- html`
1419
- <ef-video
1420
- loop
1421
- id="loop-video"
1422
- src="bars-n-tone.mp4"
1423
- sourceout="2s"
1424
- ></ef-video>
1425
- `,
1426
- container,
1427
- );
1428
- document.body.appendChild(container);
1429
-
1430
- const video = container.querySelector("#loop-video") as EFVideo;
1431
- await video.updateComplete;
1432
-
1433
- expect(video.loop).toBe(true);
1434
- expect(video.playbackController).toBeDefined();
1435
- expect(video.playbackController?.loop).toBe(true);
1436
-
1437
- container.remove();
1438
- },
1439
- );
1440
-
1441
- test(
1442
- "loop property is reactive after initialization",
1443
- { timeout: 1000 },
1444
- async () => {
1445
- const container = document.createElement("div");
1446
- render(
1447
- html`
1448
- <ef-video
1449
- id="reactive-loop-video"
1450
- src="bars-n-tone.mp4"
1451
- sourceout="2s"
1452
- ></ef-video>
1453
- `,
1454
- container,
1455
- );
1456
- document.body.appendChild(container);
1457
-
1458
- const video = container.querySelector(
1459
- "#reactive-loop-video",
1460
- ) as EFVideo;
1461
- await video.updateComplete;
1462
-
1463
- expect(video.loop).toBe(false);
1464
- expect(video.playbackController?.loop).toBe(false);
1465
-
1466
- video.loop = true;
1467
- await video.updateComplete;
1468
-
1469
- expect(video.loop).toBe(true);
1470
- expect(video.playbackController?.loop).toBe(true);
1471
-
1472
- video.loop = false;
1473
- await video.updateComplete;
1474
-
1475
- expect(video.loop).toBe(false);
1476
- expect(video.playbackController?.loop).toBe(false);
1477
-
1478
- container.remove();
1479
- },
1480
- );
1481
- });
1482
- });