@editframe/elements 0.16.7-beta.0 → 0.17.6-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.
- package/README.md +30 -0
- package/dist/DecoderResetFrequency.test.d.ts +1 -0
- package/dist/DecoderResetRecovery.test.d.ts +1 -0
- package/dist/DelayedLoadingState.d.ts +48 -0
- package/dist/DelayedLoadingState.integration.test.d.ts +1 -0
- package/dist/DelayedLoadingState.js +113 -0
- package/dist/DelayedLoadingState.test.d.ts +1 -0
- package/dist/EF_FRAMEGEN.d.ts +10 -1
- package/dist/EF_FRAMEGEN.js +199 -179
- package/dist/EF_INTERACTIVE.js +2 -6
- package/dist/EF_RENDERING.js +1 -3
- package/dist/JitTranscodingClient.browsertest.d.ts +1 -0
- package/dist/JitTranscodingClient.d.ts +167 -0
- package/dist/JitTranscodingClient.js +373 -0
- package/dist/JitTranscodingClient.test.d.ts +1 -0
- package/dist/LoadingDebounce.test.d.ts +1 -0
- package/dist/LoadingIndicator.browsertest.d.ts +0 -0
- package/dist/ManualScrubTest.test.d.ts +1 -0
- package/dist/ScrubResolvedFlashing.test.d.ts +1 -0
- package/dist/ScrubTrackIntegration.test.d.ts +1 -0
- package/dist/ScrubTrackManager.d.ts +96 -0
- package/dist/ScrubTrackManager.js +216 -0
- package/dist/ScrubTrackManager.test.d.ts +1 -0
- package/dist/SegmentSwitchLoading.test.d.ts +1 -0
- package/dist/VideoSeekFlashing.browsertest.d.ts +0 -0
- package/dist/VideoStuckDiagnostic.test.d.ts +1 -0
- package/dist/elements/CrossUpdateController.js +13 -15
- package/dist/elements/EFAudio.browsertest.d.ts +0 -0
- package/dist/elements/EFAudio.d.ts +1 -1
- package/dist/elements/EFAudio.js +30 -43
- package/dist/elements/EFCaptions.js +337 -373
- package/dist/elements/EFImage.js +64 -90
- package/dist/elements/EFMedia.d.ts +98 -33
- package/dist/elements/EFMedia.js +1169 -678
- package/dist/elements/EFSourceMixin.js +31 -48
- package/dist/elements/EFTemporal.d.ts +1 -0
- package/dist/elements/EFTemporal.js +266 -360
- package/dist/elements/EFTimegroup.d.ts +3 -1
- package/dist/elements/EFTimegroup.js +262 -323
- package/dist/elements/EFVideo.browsertest.d.ts +0 -0
- package/dist/elements/EFVideo.d.ts +90 -2
- package/dist/elements/EFVideo.js +408 -111
- package/dist/elements/EFWaveform.js +375 -411
- package/dist/elements/FetchMixin.js +14 -24
- package/dist/elements/MediaController.d.ts +30 -0
- package/dist/elements/TargetController.js +130 -156
- package/dist/elements/TimegroupController.js +17 -19
- package/dist/elements/durationConverter.js +15 -4
- package/dist/elements/parseTimeToMs.js +4 -10
- package/dist/elements/printTaskStatus.d.ts +2 -0
- package/dist/elements/printTaskStatus.js +11 -0
- package/dist/elements/updateAnimations.js +39 -59
- package/dist/getRenderInfo.js +58 -67
- package/dist/gui/ContextMixin.js +203 -288
- package/dist/gui/EFConfiguration.js +27 -43
- package/dist/gui/EFFilmstrip.js +440 -620
- package/dist/gui/EFFitScale.js +112 -135
- package/dist/gui/EFFocusOverlay.js +45 -61
- package/dist/gui/EFPreview.js +30 -49
- package/dist/gui/EFScrubber.js +78 -99
- package/dist/gui/EFTimeDisplay.js +49 -70
- package/dist/gui/EFToggleLoop.js +17 -34
- package/dist/gui/EFTogglePlay.js +37 -58
- package/dist/gui/EFWorkbench.js +66 -88
- package/dist/gui/TWMixin.js +2 -48
- package/dist/gui/TWMixin2.js +31 -0
- package/dist/gui/efContext.js +2 -6
- package/dist/gui/fetchContext.js +1 -3
- package/dist/gui/focusContext.js +1 -3
- package/dist/gui/focusedElementContext.js +2 -6
- package/dist/gui/playingContext.js +1 -4
- package/dist/index.js +5 -30
- package/dist/msToTimeCode.js +11 -13
- package/dist/style.css +2 -1
- package/package.json +3 -3
- package/src/elements/EFAudio.browsertest.ts +569 -0
- package/src/elements/EFAudio.ts +4 -6
- package/src/elements/EFCaptions.browsertest.ts +0 -1
- package/src/elements/EFImage.browsertest.ts +0 -1
- package/src/elements/EFMedia.browsertest.ts +147 -115
- package/src/elements/EFMedia.ts +1339 -307
- package/src/elements/EFTemporal.browsertest.ts +0 -1
- package/src/elements/EFTemporal.ts +11 -0
- package/src/elements/EFTimegroup.ts +73 -10
- package/src/elements/EFVideo.browsertest.ts +680 -0
- package/src/elements/EFVideo.ts +729 -50
- package/src/elements/EFWaveform.ts +4 -4
- package/src/elements/MediaController.ts +108 -0
- package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
- package/src/elements/printTaskStatus.ts +16 -0
- package/src/elements/updateAnimations.ts +6 -0
- package/src/gui/TWMixin.ts +10 -3
- package/test/EFVideo.frame-tasks.browsertest.ts +524 -0
- package/test/EFVideo.framegen.browsertest.ts +118 -0
- package/test/createJitTestClips.ts +293 -0
- package/test/useAssetMSW.ts +49 -0
- package/test/useMSW.ts +31 -0
- package/types.json +1 -1
- package/dist/gui/TWMixin.css.js +0 -4
- /package/dist/elements/{TargetController.test.d.ts → TargetController.browsertest.d.ts} +0 -0
- /package/src/elements/{TargetController.test.ts → TargetController.browsertest.ts} +0 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { html, render } from "lit";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { assetMSWHandlers } from "../../test/useAssetMSW.js";
|
|
4
|
+
import { useMSW } 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
|
+
describe("EFVideo", () => {
|
|
12
|
+
const worker = useMSW();
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// Clean up DOM and localStorage
|
|
16
|
+
while (document.body.children.length) {
|
|
17
|
+
document.body.children[0]?.remove();
|
|
18
|
+
}
|
|
19
|
+
localStorage.clear();
|
|
20
|
+
|
|
21
|
+
// Set up centralized MSW handlers to proxy requests to test assets
|
|
22
|
+
worker.use(...assetMSWHandlers);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Clean up any remaining elements
|
|
27
|
+
const videos = document.querySelectorAll("ef-video");
|
|
28
|
+
for (const video of videos) {
|
|
29
|
+
video.remove();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("basic rendering", () => {
|
|
34
|
+
test("should be defined and render canvas", async () => {
|
|
35
|
+
const element = document.createElement("ef-video");
|
|
36
|
+
document.body.appendChild(element);
|
|
37
|
+
|
|
38
|
+
// Wait for element to render
|
|
39
|
+
await element.updateComplete;
|
|
40
|
+
|
|
41
|
+
expect(element.tagName).toBe("EF-VIDEO");
|
|
42
|
+
expect(element.canvasElement).toBeDefined();
|
|
43
|
+
expect(element.canvasElement?.tagName).toBe("CANVAS");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("canvas has correct default properties", async () => {
|
|
47
|
+
const container = document.createElement("div");
|
|
48
|
+
render(html`<ef-video></ef-video>`, container);
|
|
49
|
+
document.body.appendChild(container);
|
|
50
|
+
|
|
51
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
52
|
+
|
|
53
|
+
// Wait for element to render
|
|
54
|
+
await video.updateComplete;
|
|
55
|
+
|
|
56
|
+
const canvas = video.canvasElement;
|
|
57
|
+
|
|
58
|
+
expect(canvas).toBeDefined();
|
|
59
|
+
expect(canvas?.width).toBeGreaterThan(0);
|
|
60
|
+
expect(canvas?.height).toBeGreaterThan(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("canvas inherits styling correctly", async () => {
|
|
64
|
+
const container = document.createElement("div");
|
|
65
|
+
render(
|
|
66
|
+
html`
|
|
67
|
+
<ef-video style="width: 640px; height: 360px;"></ef-video>
|
|
68
|
+
`,
|
|
69
|
+
container,
|
|
70
|
+
);
|
|
71
|
+
document.body.appendChild(container);
|
|
72
|
+
|
|
73
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
74
|
+
|
|
75
|
+
// Wait for element to render
|
|
76
|
+
await video.updateComplete;
|
|
77
|
+
|
|
78
|
+
const canvas = video.canvasElement;
|
|
79
|
+
|
|
80
|
+
expect(canvas).toBeDefined();
|
|
81
|
+
// Canvas should inherit the styling
|
|
82
|
+
const computedStyle = window.getComputedStyle(canvas!);
|
|
83
|
+
expect(computedStyle.width).toBe("640px");
|
|
84
|
+
expect(computedStyle.height).toBe("360px");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("video asset integration", () => {
|
|
89
|
+
test("integrates with video asset loading", async () => {
|
|
90
|
+
const container = document.createElement("div");
|
|
91
|
+
render(
|
|
92
|
+
html`
|
|
93
|
+
<ef-preview>
|
|
94
|
+
<ef-video src="/test-assets/media/bars-n-tone2.mp4" mode="asset"></ef-video>
|
|
95
|
+
</ef-preview>
|
|
96
|
+
`,
|
|
97
|
+
container,
|
|
98
|
+
);
|
|
99
|
+
document.body.appendChild(container);
|
|
100
|
+
|
|
101
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
102
|
+
await video.updateComplete;
|
|
103
|
+
|
|
104
|
+
// Wait for fragment index to load
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
106
|
+
|
|
107
|
+
expect(video.src).toBe("/test-assets/media/bars-n-tone2.mp4");
|
|
108
|
+
|
|
109
|
+
// The video should have loaded successfully and have a duration > 0
|
|
110
|
+
// We don't test for specific duration since real assets may vary
|
|
111
|
+
expect(video.intrinsicDurationMs).toBeGreaterThan(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("handles missing video asset gracefully", async () => {
|
|
115
|
+
const container = document.createElement("div");
|
|
116
|
+
render(
|
|
117
|
+
html`
|
|
118
|
+
<ef-preview>
|
|
119
|
+
<ef-video src="/nonexistent.mp4"></ef-video>
|
|
120
|
+
</ef-preview>
|
|
121
|
+
`,
|
|
122
|
+
container,
|
|
123
|
+
);
|
|
124
|
+
document.body.appendChild(container);
|
|
125
|
+
|
|
126
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
127
|
+
|
|
128
|
+
// Should not throw when video asset is missing
|
|
129
|
+
expect(() => {
|
|
130
|
+
video.paintTask.run();
|
|
131
|
+
}).not.toThrow();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("frame painting and canvas updates", () => {
|
|
136
|
+
test("canvas dimensions update when frame dimensions change", async () => {
|
|
137
|
+
const container = document.createElement("div");
|
|
138
|
+
render(html`<ef-video></ef-video>`, container);
|
|
139
|
+
document.body.appendChild(container);
|
|
140
|
+
|
|
141
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
142
|
+
|
|
143
|
+
// Wait for element to render
|
|
144
|
+
await video.updateComplete;
|
|
145
|
+
|
|
146
|
+
const canvas = video.canvasElement!;
|
|
147
|
+
|
|
148
|
+
// Mock a video frame with specific dimensions
|
|
149
|
+
const mockFrame = {
|
|
150
|
+
codedWidth: 1920,
|
|
151
|
+
codedHeight: 1080,
|
|
152
|
+
format: "RGBA",
|
|
153
|
+
timestamp: 0,
|
|
154
|
+
close: vi.fn(),
|
|
155
|
+
} as unknown as VideoFrame;
|
|
156
|
+
|
|
157
|
+
// Simulate frame painting (this would normally happen through paintTask)
|
|
158
|
+
const ctx = canvas.getContext("2d");
|
|
159
|
+
if (ctx && mockFrame.codedWidth && mockFrame.codedHeight) {
|
|
160
|
+
canvas.width = mockFrame.codedWidth;
|
|
161
|
+
canvas.height = mockFrame.codedHeight;
|
|
162
|
+
// Mock drawing the frame
|
|
163
|
+
ctx.fillStyle = "red";
|
|
164
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
expect(canvas.width).toBe(1920);
|
|
168
|
+
expect(canvas.height).toBe(1080);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("handles frame painting with null format gracefully", async () => {
|
|
172
|
+
const container = document.createElement("div");
|
|
173
|
+
render(html`<ef-video></ef-video>`, container);
|
|
174
|
+
document.body.appendChild(container);
|
|
175
|
+
|
|
176
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
177
|
+
|
|
178
|
+
// Wait for element to render
|
|
179
|
+
await video.updateComplete;
|
|
180
|
+
|
|
181
|
+
const canvas = video.canvasElement!;
|
|
182
|
+
|
|
183
|
+
// Mock a frame with null format (edge case)
|
|
184
|
+
const mockFrame = {
|
|
185
|
+
codedWidth: 640,
|
|
186
|
+
codedHeight: 480,
|
|
187
|
+
format: null,
|
|
188
|
+
timestamp: 0,
|
|
189
|
+
close: vi.fn(),
|
|
190
|
+
} as unknown as VideoFrame;
|
|
191
|
+
|
|
192
|
+
const ctx = canvas.getContext("2d");
|
|
193
|
+
|
|
194
|
+
// Should handle null format gracefully
|
|
195
|
+
expect(() => {
|
|
196
|
+
if (ctx && mockFrame.format === null) {
|
|
197
|
+
console.warn("Frame format is null", mockFrame);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}).not.toThrow();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("canvas context is available for drawing", async () => {
|
|
204
|
+
const container = document.createElement("div");
|
|
205
|
+
render(html`<ef-video></ef-video>`, container);
|
|
206
|
+
document.body.appendChild(container);
|
|
207
|
+
|
|
208
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
209
|
+
|
|
210
|
+
// Wait for element to render
|
|
211
|
+
await video.updateComplete;
|
|
212
|
+
|
|
213
|
+
const canvas = video.canvasElement!;
|
|
214
|
+
const ctx = canvas.getContext("2d");
|
|
215
|
+
|
|
216
|
+
expect(ctx).toBeDefined();
|
|
217
|
+
expect(ctx).toBeInstanceOf(CanvasRenderingContext2D);
|
|
218
|
+
|
|
219
|
+
// Test that we can draw on the canvas
|
|
220
|
+
expect(() => {
|
|
221
|
+
ctx!.fillStyle = "blue";
|
|
222
|
+
ctx!.fillRect(0, 0, 100, 100);
|
|
223
|
+
}).not.toThrow();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("decoder lock scenarios", () => {
|
|
228
|
+
test("handles concurrent paint attempts safely", async () => {
|
|
229
|
+
const container = document.createElement("div");
|
|
230
|
+
render(html`<ef-video></ef-video>`, container);
|
|
231
|
+
document.body.appendChild(container);
|
|
232
|
+
|
|
233
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
234
|
+
|
|
235
|
+
// Access the private decoder lock through reflection for testing
|
|
236
|
+
const decoderLockDescriptor = Object.getOwnPropertyDescriptor(
|
|
237
|
+
Object.getPrototypeOf(video),
|
|
238
|
+
"#decoderLock",
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Simulate the decoder being in use
|
|
242
|
+
if (decoderLockDescriptor) {
|
|
243
|
+
// We can't directly access private fields in tests, but we can test
|
|
244
|
+
// that multiple paint calls don't cause issues
|
|
245
|
+
const paintPromise1 = video.paintTask.run();
|
|
246
|
+
const paintPromise2 = video.paintTask.run();
|
|
247
|
+
const paintPromise3 = video.paintTask.run();
|
|
248
|
+
|
|
249
|
+
// All should complete without throwing
|
|
250
|
+
await expect(
|
|
251
|
+
Promise.allSettled([paintPromise1, paintPromise2, paintPromise3]),
|
|
252
|
+
).resolves.toBeDefined();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("paintTask handles missing canvas gracefully", () => {
|
|
257
|
+
const container = document.createElement("div");
|
|
258
|
+
render(html`<ef-video></ef-video>`, container);
|
|
259
|
+
document.body.appendChild(container);
|
|
260
|
+
|
|
261
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
262
|
+
|
|
263
|
+
// Remove canvas to test edge case
|
|
264
|
+
const canvas = video.canvasElement;
|
|
265
|
+
canvas?.remove();
|
|
266
|
+
|
|
267
|
+
// Paint task should handle missing canvas
|
|
268
|
+
expect(() => {
|
|
269
|
+
video.paintTask.run();
|
|
270
|
+
}).not.toThrow();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("handles paint task with no video asset", () => {
|
|
274
|
+
const container = document.createElement("div");
|
|
275
|
+
render(html`<ef-video></ef-video>`, container);
|
|
276
|
+
document.body.appendChild(container);
|
|
277
|
+
|
|
278
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
279
|
+
|
|
280
|
+
// Paint task should handle missing video asset gracefully
|
|
281
|
+
expect(() => {
|
|
282
|
+
video.paintTask.run();
|
|
283
|
+
}).not.toThrow();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("frame task integration", () => {
|
|
288
|
+
test("frameTask coordinates all required tasks", async () => {
|
|
289
|
+
const container = document.createElement("div");
|
|
290
|
+
render(
|
|
291
|
+
html`
|
|
292
|
+
<ef-preview>
|
|
293
|
+
<ef-video src="/test-video.mp4"></ef-video>
|
|
294
|
+
</ef-preview>
|
|
295
|
+
`,
|
|
296
|
+
container,
|
|
297
|
+
);
|
|
298
|
+
document.body.appendChild(container);
|
|
299
|
+
|
|
300
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
301
|
+
|
|
302
|
+
// frameTask should complete without errors even when other tasks fail
|
|
303
|
+
expect(() => {
|
|
304
|
+
video.frameTask.run();
|
|
305
|
+
}).not.toThrow();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("frameTask handles missing dependencies", () => {
|
|
309
|
+
const container = document.createElement("div");
|
|
310
|
+
render(html`<ef-video></ef-video>`, container);
|
|
311
|
+
document.body.appendChild(container);
|
|
312
|
+
|
|
313
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
314
|
+
|
|
315
|
+
// Should handle missing dependencies gracefully
|
|
316
|
+
expect(() => {
|
|
317
|
+
video.frameTask.run();
|
|
318
|
+
}).not.toThrow();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("error handling and edge cases", () => {
|
|
323
|
+
test("handles seek to invalid time", () => {
|
|
324
|
+
const container = document.createElement("div");
|
|
325
|
+
render(html`<ef-video></ef-video>`, container);
|
|
326
|
+
document.body.appendChild(container);
|
|
327
|
+
|
|
328
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
329
|
+
|
|
330
|
+
// Should handle invalid seek times gracefully
|
|
331
|
+
expect(() => {
|
|
332
|
+
video.desiredSeekTimeMs = -1000; // Invalid negative time
|
|
333
|
+
video.paintTask.run();
|
|
334
|
+
}).not.toThrow();
|
|
335
|
+
|
|
336
|
+
expect(() => {
|
|
337
|
+
video.desiredSeekTimeMs = Number.POSITIVE_INFINITY;
|
|
338
|
+
video.paintTask.run();
|
|
339
|
+
}).not.toThrow();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("handles video element removal during playback", () => {
|
|
343
|
+
const container = document.createElement("div");
|
|
344
|
+
render(html`<ef-video></ef-video>`, container);
|
|
345
|
+
document.body.appendChild(container);
|
|
346
|
+
|
|
347
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
348
|
+
|
|
349
|
+
// Start some operations
|
|
350
|
+
video.paintTask.run();
|
|
351
|
+
|
|
352
|
+
// Remove element
|
|
353
|
+
video.remove();
|
|
354
|
+
|
|
355
|
+
// Should not cause errors
|
|
356
|
+
expect(() => {
|
|
357
|
+
video.paintTask.run();
|
|
358
|
+
}).not.toThrow();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("handles canvas context loss gracefully", async () => {
|
|
362
|
+
const container = document.createElement("div");
|
|
363
|
+
render(html`<ef-video></ef-video>`, container);
|
|
364
|
+
document.body.appendChild(container);
|
|
365
|
+
|
|
366
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
367
|
+
|
|
368
|
+
// Wait for element to render
|
|
369
|
+
await video.updateComplete;
|
|
370
|
+
|
|
371
|
+
const canvas = video.canvasElement!;
|
|
372
|
+
|
|
373
|
+
// Simulate context loss by making getContext return null
|
|
374
|
+
const originalGetContext = canvas.getContext;
|
|
375
|
+
canvas.getContext = vi.fn().mockReturnValue(null);
|
|
376
|
+
|
|
377
|
+
// Should handle context loss gracefully
|
|
378
|
+
expect(() => {
|
|
379
|
+
video.paintTask.run();
|
|
380
|
+
}).not.toThrow();
|
|
381
|
+
|
|
382
|
+
// Restore original method
|
|
383
|
+
canvas.getContext = originalGetContext;
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("integration with timegroups", () => {
|
|
388
|
+
test("integrates correctly within timegroup structure", async () => {
|
|
389
|
+
const container = document.createElement("div");
|
|
390
|
+
render(
|
|
391
|
+
html`
|
|
392
|
+
<ef-preview>
|
|
393
|
+
<ef-timegroup mode="sequence">
|
|
394
|
+
<ef-video src="/test-assets/media/bars-n-tone2.mp4" mode="asset"></ef-video>
|
|
395
|
+
</ef-timegroup>
|
|
396
|
+
</ef-preview>
|
|
397
|
+
`,
|
|
398
|
+
container,
|
|
399
|
+
);
|
|
400
|
+
document.body.appendChild(container);
|
|
401
|
+
|
|
402
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
403
|
+
const timegroup = container.querySelector("ef-timegroup");
|
|
404
|
+
await video.updateComplete;
|
|
405
|
+
|
|
406
|
+
// Wait for fragment index to load with longer timeout
|
|
407
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
408
|
+
|
|
409
|
+
expect(timegroup).toBeDefined();
|
|
410
|
+
|
|
411
|
+
// The video should have loaded successfully within the timegroup
|
|
412
|
+
// We test that it has a valid duration instead of a specific value
|
|
413
|
+
// Allow for race conditions in test environment
|
|
414
|
+
if (video.intrinsicDurationMs === 0) {
|
|
415
|
+
// If not loaded yet, wait a bit more
|
|
416
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
417
|
+
}
|
|
418
|
+
expect(video.intrinsicDurationMs).toBeGreaterThan(0);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe("scrub track integration", () => {
|
|
423
|
+
test("should initialize scrub track manager for JIT transcode mode", async () => {
|
|
424
|
+
const container = document.createElement("div");
|
|
425
|
+
render(
|
|
426
|
+
html`
|
|
427
|
+
<ef-preview>
|
|
428
|
+
<ef-video src="http://example.com/video.mp4"></ef-video>
|
|
429
|
+
</ef-preview>
|
|
430
|
+
`,
|
|
431
|
+
container,
|
|
432
|
+
);
|
|
433
|
+
document.body.appendChild(container);
|
|
434
|
+
|
|
435
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
436
|
+
await video.updateComplete;
|
|
437
|
+
|
|
438
|
+
// Give the async initialization time to complete
|
|
439
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
440
|
+
|
|
441
|
+
// For JIT transcode mode, scrub track manager should be initialized
|
|
442
|
+
expect(video.scrubTrackManager).toBeDefined();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("should not initialize scrub track manager for asset mode", async () => {
|
|
446
|
+
const container = document.createElement("div");
|
|
447
|
+
render(
|
|
448
|
+
html`
|
|
449
|
+
<ef-preview>
|
|
450
|
+
<ef-video src="/@ef-abc123/video.mp4" mode="asset"></ef-video>
|
|
451
|
+
</ef-preview>
|
|
452
|
+
`,
|
|
453
|
+
container,
|
|
454
|
+
);
|
|
455
|
+
document.body.appendChild(container);
|
|
456
|
+
|
|
457
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
458
|
+
await video.updateComplete;
|
|
459
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
460
|
+
|
|
461
|
+
// For asset mode, scrub track manager should not be initialized
|
|
462
|
+
expect(video.scrubTrackManager).toBeUndefined();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("should expose scrub track performance metrics", async () => {
|
|
466
|
+
const container = document.createElement("div");
|
|
467
|
+
render(
|
|
468
|
+
html`
|
|
469
|
+
<ef-preview>
|
|
470
|
+
<ef-video src="http://example.com/video.mp4"></ef-video>
|
|
471
|
+
</ef-preview>
|
|
472
|
+
`,
|
|
473
|
+
container,
|
|
474
|
+
);
|
|
475
|
+
document.body.appendChild(container);
|
|
476
|
+
|
|
477
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
478
|
+
await video.updateComplete;
|
|
479
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
480
|
+
|
|
481
|
+
const stats = video.getScrubTrackStats();
|
|
482
|
+
|
|
483
|
+
if (video.scrubTrackManager) {
|
|
484
|
+
expect(stats).not.toBeNull();
|
|
485
|
+
expect(typeof stats?.hits).toBe("number");
|
|
486
|
+
expect(typeof stats?.misses).toBe("number");
|
|
487
|
+
expect(typeof stats?.hitRate).toBe("number");
|
|
488
|
+
} else {
|
|
489
|
+
expect(stats).toBeNull();
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("should return null stats when no scrub track manager exists", async () => {
|
|
494
|
+
const container = document.createElement("div");
|
|
495
|
+
render(
|
|
496
|
+
html`
|
|
497
|
+
<ef-preview>
|
|
498
|
+
<ef-video src="/@ef-abc123/video.mp4" mode="asset"></ef-video>
|
|
499
|
+
</ef-preview>
|
|
500
|
+
`,
|
|
501
|
+
container,
|
|
502
|
+
);
|
|
503
|
+
document.body.appendChild(container);
|
|
504
|
+
|
|
505
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
506
|
+
await video.updateComplete;
|
|
507
|
+
|
|
508
|
+
const stats = video.getScrubTrackStats();
|
|
509
|
+
expect(stats).toBeNull();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("should have canvas element available", async () => {
|
|
513
|
+
const container = document.createElement("div");
|
|
514
|
+
render(html`<ef-video></ef-video>`, container);
|
|
515
|
+
document.body.appendChild(container);
|
|
516
|
+
|
|
517
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
518
|
+
await video.updateComplete;
|
|
519
|
+
|
|
520
|
+
const canvas = video.canvasElement;
|
|
521
|
+
expect(canvas).toBeDefined();
|
|
522
|
+
expect(canvas?.tagName).toBe("CANVAS");
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("should clean up scrub track manager on disconnect", async () => {
|
|
526
|
+
const container = document.createElement("div");
|
|
527
|
+
render(
|
|
528
|
+
html`
|
|
529
|
+
<ef-preview>
|
|
530
|
+
<ef-video src="http://example.com/video.mp4"></ef-video>
|
|
531
|
+
</ef-preview>
|
|
532
|
+
`,
|
|
533
|
+
container,
|
|
534
|
+
);
|
|
535
|
+
document.body.appendChild(container);
|
|
536
|
+
|
|
537
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
538
|
+
await video.updateComplete;
|
|
539
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
540
|
+
|
|
541
|
+
const hadScrubManager = !!video.scrubTrackManager;
|
|
542
|
+
|
|
543
|
+
// Simulate disconnect
|
|
544
|
+
video.remove();
|
|
545
|
+
|
|
546
|
+
// If there was a scrub manager, it should have been cleaned up
|
|
547
|
+
// We can't directly test the cleanup call, but we can verify the element is disconnected
|
|
548
|
+
expect(video.isConnected).toBe(false);
|
|
549
|
+
|
|
550
|
+
// The scrub manager should still exist but be cleaned up internally
|
|
551
|
+
if (hadScrubManager) {
|
|
552
|
+
expect(video.scrubTrackManager).toBeDefined();
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe("loading indicator", () => {
|
|
558
|
+
test("should not show loading indicator for operations completing under 250ms", async () => {
|
|
559
|
+
const container = document.createElement("div");
|
|
560
|
+
render(html`<ef-video></ef-video>`, container);
|
|
561
|
+
document.body.appendChild(container);
|
|
562
|
+
|
|
563
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
564
|
+
await video.updateComplete;
|
|
565
|
+
|
|
566
|
+
// Start a fast operation
|
|
567
|
+
video.startDelayedLoading("test-fast", "Fast operation");
|
|
568
|
+
|
|
569
|
+
// Clear it quickly (under 250ms)
|
|
570
|
+
setTimeout(() => {
|
|
571
|
+
video.clearDelayedLoading("test-fast");
|
|
572
|
+
}, 100);
|
|
573
|
+
|
|
574
|
+
// Wait past the delay threshold
|
|
575
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
576
|
+
|
|
577
|
+
expect(video.loadingState.isLoading).toBe(false);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("should show loading indicator only after 250ms for slow operations", async () => {
|
|
581
|
+
const container = document.createElement("div");
|
|
582
|
+
render(html`<ef-video></ef-video>`, container);
|
|
583
|
+
document.body.appendChild(container);
|
|
584
|
+
|
|
585
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
586
|
+
await video.updateComplete;
|
|
587
|
+
|
|
588
|
+
// Start a slow operation
|
|
589
|
+
video.startDelayedLoading("test-slow", "Slow operation");
|
|
590
|
+
|
|
591
|
+
// Should not be loading immediately
|
|
592
|
+
expect(video.loadingState.isLoading).toBe(false);
|
|
593
|
+
|
|
594
|
+
// Wait past the delay threshold
|
|
595
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
596
|
+
|
|
597
|
+
// Should now be loading
|
|
598
|
+
expect(video.loadingState.isLoading).toBe(true);
|
|
599
|
+
expect(video.loadingState.message).toBe("Slow operation");
|
|
600
|
+
|
|
601
|
+
// Clear the loading
|
|
602
|
+
video.clearDelayedLoading("test-slow");
|
|
603
|
+
|
|
604
|
+
// Should stop loading
|
|
605
|
+
expect(video.loadingState.isLoading).toBe(false);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("should handle multiple concurrent loading operations", async () => {
|
|
609
|
+
const container = document.createElement("div");
|
|
610
|
+
render(html`<ef-video></ef-video>`, container);
|
|
611
|
+
document.body.appendChild(container);
|
|
612
|
+
|
|
613
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
614
|
+
await video.updateComplete;
|
|
615
|
+
|
|
616
|
+
// Start multiple operations
|
|
617
|
+
video.startDelayedLoading("op1", "Operation 1");
|
|
618
|
+
video.startDelayedLoading("op2", "Operation 2");
|
|
619
|
+
|
|
620
|
+
// Wait past delay threshold
|
|
621
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
622
|
+
|
|
623
|
+
// Should be loading
|
|
624
|
+
expect(video.loadingState.isLoading).toBe(true);
|
|
625
|
+
|
|
626
|
+
// Clear one operation
|
|
627
|
+
video.clearDelayedLoading("op1");
|
|
628
|
+
|
|
629
|
+
// Should still be loading (op2 still active)
|
|
630
|
+
expect(video.loadingState.isLoading).toBe(true);
|
|
631
|
+
|
|
632
|
+
// Clear second operation
|
|
633
|
+
video.clearDelayedLoading("op2");
|
|
634
|
+
|
|
635
|
+
// Should stop loading
|
|
636
|
+
expect(video.loadingState.isLoading).toBe(false);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("should not show loading for background operations", async () => {
|
|
640
|
+
const container = document.createElement("div");
|
|
641
|
+
render(html`<ef-video></ef-video>`, container);
|
|
642
|
+
document.body.appendChild(container);
|
|
643
|
+
|
|
644
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
645
|
+
await video.updateComplete;
|
|
646
|
+
|
|
647
|
+
// Start a background operation
|
|
648
|
+
video.startDelayedLoading("bg-op", "Background operation", {
|
|
649
|
+
background: true,
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Wait past delay threshold
|
|
653
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
654
|
+
|
|
655
|
+
// Should not show loading UI for background operations
|
|
656
|
+
expect(video.loadingState.isLoading).toBe(false);
|
|
657
|
+
|
|
658
|
+
// Clear the operation
|
|
659
|
+
video.clearDelayedLoading("bg-op");
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("should properly clean up loading state on disconnect", async () => {
|
|
663
|
+
const container = document.createElement("div");
|
|
664
|
+
render(html`<ef-video></ef-video>`, container);
|
|
665
|
+
document.body.appendChild(container);
|
|
666
|
+
|
|
667
|
+
const video = container.querySelector("ef-video") as EFVideo;
|
|
668
|
+
await video.updateComplete;
|
|
669
|
+
|
|
670
|
+
// Start an operation
|
|
671
|
+
video.startDelayedLoading("cleanup-test", "Test operation");
|
|
672
|
+
|
|
673
|
+
// Disconnect the element
|
|
674
|
+
video.remove();
|
|
675
|
+
|
|
676
|
+
// Loading should be cleared
|
|
677
|
+
expect(video.loadingState.isLoading).toBe(false);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
});
|