@editframe/elements 0.18.27-beta.0 → 0.19.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 (73) hide show
  1. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +10 -0
  2. package/dist/elements/EFMedia/AssetMediaEngine.js +13 -1
  3. package/dist/elements/EFMedia/JitMediaEngine.d.ts +10 -0
  4. package/dist/elements/EFMedia/JitMediaEngine.js +12 -0
  5. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +16 -12
  6. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.d.ts +1 -1
  7. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +0 -4
  8. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +1 -1
  9. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +0 -4
  10. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +1 -1
  11. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +3 -2
  12. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +16 -12
  13. package/dist/elements/EFMedia.d.ts +2 -3
  14. package/dist/elements/EFMedia.js +0 -4
  15. package/dist/elements/EFTemporal.d.ts +9 -6
  16. package/dist/elements/EFTemporal.js +15 -12
  17. package/dist/elements/EFTimegroup.browsertest.d.ts +26 -0
  18. package/dist/elements/EFTimegroup.d.ts +13 -15
  19. package/dist/elements/EFTimegroup.js +123 -67
  20. package/dist/elements/EFVideo.d.ts +5 -1
  21. package/dist/elements/EFVideo.js +16 -8
  22. package/dist/elements/EFWaveform.js +2 -3
  23. package/dist/elements/FetchContext.browsertest.d.ts +0 -0
  24. package/dist/elements/FetchMixin.js +14 -9
  25. package/dist/elements/TimegroupController.js +2 -1
  26. package/dist/elements/updateAnimations.browsertest.d.ts +0 -0
  27. package/dist/elements/updateAnimations.d.ts +19 -9
  28. package/dist/elements/updateAnimations.js +64 -25
  29. package/dist/gui/ContextMixin.js +34 -27
  30. package/dist/gui/EFConfiguration.d.ts +1 -1
  31. package/dist/gui/EFConfiguration.js +1 -0
  32. package/dist/gui/EFFilmstrip.d.ts +1 -0
  33. package/dist/gui/EFFilmstrip.js +12 -14
  34. package/dist/gui/TWMixin.js +1 -1
  35. package/dist/style.css +1 -1
  36. package/dist/transcoding/cache/URLTokenDeduplicator.d.ts +38 -0
  37. package/dist/transcoding/cache/URLTokenDeduplicator.js +66 -0
  38. package/dist/transcoding/cache/URLTokenDeduplicator.test.d.ts +1 -0
  39. package/dist/transcoding/types/index.d.ts +10 -0
  40. package/package.json +2 -2
  41. package/src/elements/EFMedia/AssetMediaEngine.ts +16 -2
  42. package/src/elements/EFMedia/JitMediaEngine.ts +14 -0
  43. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -1
  44. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +11 -4
  45. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -4
  46. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +4 -1
  47. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -5
  48. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +2 -2
  49. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +7 -3
  50. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +11 -4
  51. package/src/elements/EFMedia.browsertest.ts +13 -4
  52. package/src/elements/EFMedia.ts +6 -10
  53. package/src/elements/EFTemporal.ts +21 -26
  54. package/src/elements/EFTimegroup.browsertest.ts +186 -2
  55. package/src/elements/EFTimegroup.ts +205 -98
  56. package/src/elements/EFVideo.browsertest.ts +53 -132
  57. package/src/elements/EFVideo.ts +26 -13
  58. package/src/elements/EFWaveform.ts +2 -3
  59. package/src/elements/FetchContext.browsertest.ts +396 -0
  60. package/src/elements/FetchMixin.ts +25 -8
  61. package/src/elements/TimegroupController.ts +2 -1
  62. package/src/elements/updateAnimations.browsertest.ts +586 -0
  63. package/src/elements/updateAnimations.ts +113 -50
  64. package/src/gui/ContextMixin.browsertest.ts +4 -9
  65. package/src/gui/ContextMixin.ts +52 -33
  66. package/src/gui/EFConfiguration.ts +1 -1
  67. package/src/gui/EFFilmstrip.ts +15 -18
  68. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +182 -0
  69. package/src/transcoding/cache/URLTokenDeduplicator.ts +101 -0
  70. package/src/transcoding/types/index.ts +11 -0
  71. package/test/EFVideo.framegen.browsertest.ts +1 -1
  72. package/test/setup.ts +2 -0
  73. package/types.json +1 -1
@@ -0,0 +1,396 @@
1
+ import { html, render } from "lit";
2
+ import { describe } 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/EFConfiguration.js";
9
+
10
+ const test = baseTest.extend({});
11
+
12
+ describe("URL Token Deduplication", () => {
13
+ test("multiple EFMedia elements with same src should share URL tokens", async ({
14
+ expect,
15
+ }) => {
16
+ // Mock fetch to track token requests
17
+ const originalFetch = window.fetch;
18
+ const tokenRequests: string[] = [];
19
+ const mediaRequests: string[] = [];
20
+
21
+ window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
22
+ const url = input.toString();
23
+
24
+ if (url.includes("/api/v1/url-token") || url.includes("/@ef-sign-url")) {
25
+ console.log("Token request:", url, init?.body);
26
+ tokenRequests.push(url);
27
+ // Mock token response
28
+ return new Response(JSON.stringify({ token: "mock-token" }), {
29
+ status: 200,
30
+ headers: { "Content-Type": "application/json" },
31
+ });
32
+ }
33
+
34
+ if (url.includes("/api/v1/transcode/")) {
35
+ console.log("Media request:", url);
36
+ mediaRequests.push(url);
37
+ // Mock media response
38
+ return new Response("mock-media-data", { status: 200 });
39
+ }
40
+
41
+ return originalFetch(input, init);
42
+ };
43
+
44
+ try {
45
+ const container = document.createElement("div");
46
+ // Create 3 EFVideo elements with the same source in a shared context
47
+ render(
48
+ html`
49
+ <ef-configuration api-host="http://localhost:3000" signing-url="/@ef-sign-url">
50
+ <ef-workbench>
51
+ <ef-video src="http://example.com/test.mp4"></ef-video>
52
+ <ef-video src="http://example.com/test.mp4"></ef-video>
53
+ <ef-video src="http://example.com/test.mp4"></ef-video>
54
+ </ef-workbench>
55
+ </ef-configuration>
56
+ `,
57
+ container,
58
+ );
59
+ document.body.appendChild(container);
60
+
61
+ const videos = container.querySelectorAll(
62
+ "ef-video",
63
+ ) as NodeListOf<EFVideo>;
64
+ const workbench = container.querySelector("ef-workbench") as any;
65
+
66
+ await workbench.updateComplete;
67
+ await Promise.all(Array.from(videos).map((v) => v.updateComplete));
68
+
69
+ // Trigger manifest requests for all videos
70
+ await Promise.all(
71
+ Array.from(videos).map(async (video) => {
72
+ try {
73
+ await video.mediaEngineTask.run();
74
+ } catch (error) {
75
+ // Expected to fail since we're mocking, but should trigger token requests
76
+ console.log("Expected error:", error);
77
+ }
78
+ }),
79
+ );
80
+
81
+ console.log("Token requests:", tokenRequests);
82
+ console.log("Media requests:", mediaRequests);
83
+
84
+ // Should only have 1 token request since all videos share the same source
85
+ // This test will currently fail, demonstrating the issue
86
+ expect(tokenRequests.length).toBe(1);
87
+
88
+ container.remove();
89
+ } finally {
90
+ window.fetch = originalFetch;
91
+ }
92
+ });
93
+
94
+ test("multiple EFMedia elements in separate context providers should share tokens globally", async ({
95
+ expect,
96
+ }) => {
97
+ // This test verifies global token deduplication across separate context providers
98
+ const originalFetch = window.fetch;
99
+ const tokenRequests: { url: string; body: any; timestamp: number }[] = [];
100
+
101
+ window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
102
+ const url = input.toString();
103
+
104
+ if (url.includes("/api/v1/url-token") || url.includes("/@ef-sign-url")) {
105
+ const bodyData = init?.body ? JSON.parse(init.body as string) : null;
106
+ const timestamp = Date.now();
107
+ console.log("Token request:", url, bodyData, `at ${timestamp}`);
108
+ tokenRequests.push({ url, body: bodyData, timestamp });
109
+
110
+ // Add small delay to make timing issues more apparent
111
+ await new Promise((resolve) => setTimeout(resolve, 10));
112
+
113
+ return new Response(JSON.stringify({ token: "mock-token" }), {
114
+ status: 200,
115
+ headers: { "Content-Type": "application/json" },
116
+ });
117
+ }
118
+
119
+ if (url.includes("/api/v1/transcode/")) {
120
+ return new Response(
121
+ JSON.stringify({
122
+ audioRenditions: [],
123
+ videoRenditions: [],
124
+ src: "test",
125
+ durationMs: 1000,
126
+ }),
127
+ {
128
+ status: 200,
129
+ headers: { "Content-Type": "application/json" },
130
+ },
131
+ );
132
+ }
133
+
134
+ return originalFetch(input, init);
135
+ };
136
+
137
+ try {
138
+ const container = document.createElement("div");
139
+ // Create multiple elements with the same source, added concurrently
140
+ container.innerHTML = `
141
+ <ef-configuration api-host="http://localhost:3000" signing-url="/@ef-sign-url">
142
+ <ef-workbench>
143
+ <ef-video src="http://example.com/test.mp4"></ef-video>
144
+ </ef-workbench>
145
+ </ef-configuration>
146
+ <ef-configuration api-host="http://localhost:3000" signing-url="/@ef-sign-url">
147
+ <ef-workbench>
148
+ <ef-video src="http://example.com/test.mp4"></ef-video>
149
+ </ef-workbench>
150
+ </ef-configuration>
151
+ <ef-configuration api-host="http://localhost:3000" signing-url="/@ef-sign-url">
152
+ <ef-workbench>
153
+ <ef-video src="http://example.com/test.mp4"></ef-video>
154
+ </ef-workbench>
155
+ </ef-configuration>
156
+ `;
157
+ document.body.appendChild(container);
158
+
159
+ const videos = container.querySelectorAll(
160
+ "ef-video",
161
+ ) as NodeListOf<EFVideo>;
162
+ const workbenches = container.querySelectorAll(
163
+ "ef-workbench",
164
+ ) as NodeListOf<any>;
165
+
166
+ await Promise.all(Array.from(workbenches).map((w) => w.updateComplete));
167
+ await Promise.all(Array.from(videos).map((v) => v.updateComplete));
168
+
169
+ // Trigger all requests simultaneously to test race conditions
170
+ await Promise.all(
171
+ Array.from(videos).map(async (video) => {
172
+ try {
173
+ await video.mediaEngineTask.run();
174
+ } catch (_error) {
175
+ // Expected due to mocking
176
+ }
177
+ }),
178
+ );
179
+
180
+ console.log(`Total token requests: ${tokenRequests.length}`);
181
+ tokenRequests.forEach((req, i) => {
182
+ console.log(`Request ${i + 1}:`, req.body, `timing: ${req.timestamp}`);
183
+ });
184
+
185
+ // Log for debugging - the real issue might be in timing/concurrency
186
+ console.log(
187
+ "Token request details:",
188
+ tokenRequests.map((r) => ({
189
+ url: r.body?.url,
190
+ params: r.body?.params,
191
+ timing: r.timestamp,
192
+ })),
193
+ );
194
+
195
+ // With global token deduplication, should only make 1 token request
196
+ // even across separate context providers
197
+ expect(tokenRequests.length).toBe(1);
198
+
199
+ container.remove();
200
+ } finally {
201
+ window.fetch = originalFetch;
202
+ }
203
+ });
204
+
205
+ test("concurrent token requests are properly deduplicated globally", async ({
206
+ expect,
207
+ }) => {
208
+ // This test simulates the race condition where multiple elements initialize simultaneously
209
+ const originalFetch = window.fetch;
210
+ const tokenRequests: { url: string; body: any; timestamp: number }[] = [];
211
+ const concurrentRequestStarts: number[] = [];
212
+
213
+ window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
214
+ const url = input.toString();
215
+
216
+ if (url.includes("/api/v1/url-token") || url.includes("/@ef-sign-url")) {
217
+ const bodyData = init?.body ? JSON.parse(init.body as string) : null;
218
+ const timestamp = Date.now();
219
+ concurrentRequestStarts.push(timestamp);
220
+ console.log("Token request started:", url, bodyData, `at ${timestamp}`);
221
+
222
+ // Simulate network delay
223
+ await new Promise((resolve) => setTimeout(resolve, 50));
224
+
225
+ tokenRequests.push({ url, body: bodyData, timestamp });
226
+ return new Response(JSON.stringify({ token: "mock-token" }), {
227
+ status: 200,
228
+ headers: { "Content-Type": "application/json" },
229
+ });
230
+ }
231
+
232
+ if (url.includes("/api/v1/transcode/")) {
233
+ return new Response(
234
+ JSON.stringify({
235
+ audioRenditions: [],
236
+ videoRenditions: [],
237
+ src: "test",
238
+ durationMs: 1000,
239
+ }),
240
+ {
241
+ status: 200,
242
+ headers: { "Content-Type": "application/json" },
243
+ },
244
+ );
245
+ }
246
+
247
+ return originalFetch(input, init);
248
+ };
249
+
250
+ try {
251
+ // Create elements and trigger simultaneous token requests
252
+ const elements: any[] = [];
253
+ const containers: HTMLElement[] = [];
254
+
255
+ // Create 5 separate context providers with identical videos
256
+ for (let i = 0; i < 5; i++) {
257
+ const container = document.createElement("div");
258
+ container.innerHTML = `
259
+ <ef-configuration api-host="http://localhost:3000" signing-url="/@ef-sign-url">
260
+ <ef-workbench>
261
+ <ef-video src="http://example.com/concurrent-test.mp4"></ef-video>
262
+ </ef-workbench>
263
+ </ef-configuration>
264
+ `;
265
+ document.body.appendChild(container);
266
+ containers.push(container);
267
+
268
+ const video = container.querySelector("ef-video");
269
+ const workbench = container.querySelector("ef-workbench");
270
+ elements.push({ video, workbench });
271
+ }
272
+
273
+ // Wait for all elements to be ready
274
+ await Promise.all(
275
+ elements.map(async ({ video, workbench }) => {
276
+ await workbench.updateComplete;
277
+ await video.updateComplete;
278
+ }),
279
+ );
280
+
281
+ // Trigger all manifest requests simultaneously to create race condition
282
+ const startTime = Date.now();
283
+ await Promise.all(
284
+ elements.map(async ({ video }) => {
285
+ try {
286
+ await video.mediaEngineTask.run();
287
+ } catch (_error) {
288
+ // Expected due to mocking
289
+ }
290
+ }),
291
+ );
292
+ const endTime = Date.now();
293
+
294
+ console.log(`All requests completed in ${endTime - startTime}ms`);
295
+ console.log(`Concurrent starts: ${concurrentRequestStarts.length}`);
296
+ console.log(`Completed token requests: ${tokenRequests.length}`);
297
+
298
+ // Verify that despite 5 concurrent elements, only 1 token was actually fetched
299
+ expect(tokenRequests.length).toBe(1);
300
+
301
+ // Cleanup
302
+ containers.forEach((container) => container.remove());
303
+ } finally {
304
+ window.fetch = originalFetch;
305
+ }
306
+ });
307
+
308
+ test("ten identical videos should use single URL token (user reported issue)", async ({
309
+ expect,
310
+ }) => {
311
+ // This test specifically reproduces the user's reported issue:
312
+ // 10 identical videos creating 10 tokens instead of sharing 1 token
313
+ const originalFetch = window.fetch;
314
+ const tokenRequests: string[] = [];
315
+
316
+ window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
317
+ const url = input.toString();
318
+
319
+ if (url.includes("/api/v1/url-token") || url.includes("/@ef-sign-url")) {
320
+ console.log("Token request for:", JSON.parse(init?.body as string));
321
+ tokenRequests.push(url);
322
+ return new Response(JSON.stringify({ token: "shared-token-for-all" }), {
323
+ status: 200,
324
+ headers: { "Content-Type": "application/json" },
325
+ });
326
+ }
327
+
328
+ if (url.includes("/api/v1/transcode/")) {
329
+ return new Response(
330
+ JSON.stringify({
331
+ audioRenditions: [],
332
+ videoRenditions: [],
333
+ src: "test",
334
+ durationMs: 1000,
335
+ }),
336
+ {
337
+ status: 200,
338
+ headers: { "Content-Type": "application/json" },
339
+ },
340
+ );
341
+ }
342
+
343
+ return originalFetch(input, init);
344
+ };
345
+
346
+ try {
347
+ const container = document.createElement("div");
348
+
349
+ // Create exactly the scenario the user described: 10 identical videos on one page
350
+ const videoElements = Array.from(
351
+ { length: 10 },
352
+ (_, i) =>
353
+ `<ef-video src="http://example.com/user-video.mp4" id="video-${i}"></ef-video>`,
354
+ ).join("\n");
355
+
356
+ container.innerHTML = `
357
+ <ef-configuration api-host="http://localhost:3000" signing-url="/@ef-sign-url">
358
+ <ef-workbench>
359
+ ${videoElements}
360
+ </ef-workbench>
361
+ </ef-configuration>
362
+ `;
363
+ document.body.appendChild(container);
364
+
365
+ const videos = container.querySelectorAll("ef-video");
366
+ const workbench = container.querySelector("ef-workbench") as any;
367
+
368
+ expect(videos.length).toBe(10);
369
+
370
+ await workbench.updateComplete;
371
+ await Promise.all(Array.from(videos).map((v: any) => v.updateComplete));
372
+
373
+ // Trigger media engine initialization for all 10 videos
374
+ await Promise.all(
375
+ Array.from(videos).map(async (video: any) => {
376
+ try {
377
+ await video.mediaEngineTask.run();
378
+ } catch (_error) {
379
+ // Expected due to mocking
380
+ }
381
+ }),
382
+ );
383
+
384
+ console.log(
385
+ `Total token requests for 10 identical videos: ${tokenRequests.length}`,
386
+ );
387
+
388
+ // This should now be 1 instead of 10, proving the deduplication works
389
+ expect(tokenRequests.length).toBe(1);
390
+
391
+ container.remove();
392
+ } finally {
393
+ window.fetch = originalFetch;
394
+ }
395
+ });
396
+ });
@@ -1,8 +1,4 @@
1
- import { consume } from "@lit/context";
2
1
  import type { LitElement } from "lit";
3
- import { state } from "lit/decorators/state.js";
4
-
5
- import { fetchContext } from "../gui/fetchContext.js";
6
2
 
7
3
  export declare class FetchMixinInterface {
8
4
  fetch: typeof fetch;
@@ -11,10 +7,31 @@ export declare class FetchMixinInterface {
11
7
  type Constructor<T = {}> = new (...args: any[]) => T;
12
8
  export function FetchMixin<T extends Constructor<LitElement>>(superClass: T) {
13
9
  class FetchElement extends superClass {
14
- @consume({ context: fetchContext, subscribe: true })
15
- @state()
16
- fetch: (url: string, init?: RequestInit) => Promise<Response> =
17
- fetch.bind(window);
10
+ fetch = (url: string, init?: RequestInit): Promise<Response> => {
11
+ try {
12
+ // Look for context providers up the DOM tree
13
+ const workbench = this.closest("ef-workbench") as any;
14
+ if (workbench?.fetch) {
15
+ return workbench.fetch(url, init);
16
+ }
17
+
18
+ const preview = this.closest("ef-preview") as any;
19
+ if (preview?.fetch) {
20
+ return preview.fetch(url, init);
21
+ }
22
+
23
+ const configuration = this.closest("ef-configuration") as any;
24
+ if (configuration?.fetch) {
25
+ return configuration.fetch(url, init);
26
+ }
27
+
28
+ // Fallback to window.fetch
29
+ return window.fetch(url, init);
30
+ } catch (error) {
31
+ console.error("FetchMixin error", url, error);
32
+ throw error;
33
+ }
34
+ };
18
35
  }
19
36
 
20
37
  return FetchElement as Constructor<FetchMixinInterface> & T;
@@ -19,7 +19,8 @@ export class TimegroupController implements ReactiveController {
19
19
 
20
20
  hostUpdated(): void {
21
21
  this.child.requestUpdate();
22
- this.child.currentTimeMs =
22
+ const newChildTimeMs =
23
23
  this.host.currentTimeMs - (this.child.startTimeMs ?? 0);
24
+ this.child.currentTimeMs = newChildTimeMs;
24
25
  }
25
26
  }