@editframe/elements 0.18.21-beta.0 → 0.18.23-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 (34) hide show
  1. package/dist/elements/EFAudio.d.ts +1 -12
  2. package/dist/elements/EFAudio.js +3 -18
  3. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -1
  4. package/dist/elements/EFMedia/AssetMediaEngine.js +3 -3
  5. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +15 -9
  6. package/dist/elements/EFMedia/BufferedSeekingInput.js +76 -78
  7. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +12 -10
  8. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +2 -18
  9. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +12 -10
  10. package/dist/elements/EFTimegroup.d.ts +4 -4
  11. package/dist/elements/EFTimegroup.js +52 -39
  12. package/dist/elements/EFVideo.d.ts +1 -32
  13. package/dist/elements/EFVideo.js +13 -51
  14. package/dist/elements/SampleBuffer.js +1 -1
  15. package/package.json +2 -2
  16. package/src/elements/EFAudio.browsertest.ts +0 -3
  17. package/src/elements/EFAudio.ts +3 -22
  18. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +39 -1
  19. package/src/elements/EFMedia/AssetMediaEngine.ts +5 -4
  20. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +90 -185
  21. package/src/elements/EFMedia/BufferedSeekingInput.ts +119 -130
  22. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +21 -21
  23. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +10 -5
  24. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +33 -34
  25. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +22 -20
  26. package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +0 -3
  27. package/src/elements/EFMedia.browsertest.ts +72 -60
  28. package/src/elements/EFTimegroup.browsertest.ts +9 -4
  29. package/src/elements/EFTimegroup.ts +79 -55
  30. package/src/elements/EFVideo.browsertest.ts +172 -160
  31. package/src/elements/EFVideo.ts +17 -73
  32. package/src/elements/SampleBuffer.ts +1 -2
  33. package/test/EFVideo.framegen.browsertest.ts +0 -54
  34. package/types.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { css } from "lit";
2
2
  import { customElement } from "lit/decorators.js";
3
3
  import type { VideoSample } from "mediabunny";
4
- import { describe, vi } from "vitest";
4
+ import { afterEach, beforeEach, describe, vi } from "vitest";
5
5
  import { test as baseTest } from "../../test/useMSW.js";
6
6
 
7
7
  import type { EFConfiguration } from "../gui/EFConfiguration.js";
@@ -97,15 +97,11 @@ describe("JIT Media Engine", () => {
97
97
  jitVideo,
98
98
  expect,
99
99
  }) => {
100
- // Set the time on the timegroup - this should trigger proper synchronization
100
+ await timegroup.waitForMediaDurations();
101
101
  timegroup.currentTimeMs = 2200;
102
-
102
+ await timegroup.seekTask.taskComplete;
103
103
  const sample = await jitVideo.videoSeekTask.taskComplete;
104
-
105
- expect(sample).toBeDefined();
106
- // Based on the pattern: 0ms→0, 3000ms→2.96, 5000ms→4.96
107
- // For 2200ms, we expect timestamp 2.16
108
- expect(sample?.timestamp).toEqual(2.16);
104
+ expect(sample?.timestamp).toBeCloseTo(2.2, 1);
109
105
  });
110
106
  });
111
107
 
@@ -115,8 +111,25 @@ describe("JIT Media Engine", () => {
115
111
  jitVideo,
116
112
  expect,
117
113
  }) => {
114
+ // Debug: Check what segment should be loaded for 0ms
115
+ const mediaEngine = await (jitVideo as any).mediaEngineTask.taskComplete;
116
+ const videoRendition = mediaEngine?.getVideoRendition();
117
+ const expectedSegmentId = mediaEngine?.computeSegmentId(
118
+ 0,
119
+ videoRendition,
120
+ );
121
+ console.log(`MediaEngine.computeSegmentId(0ms) = ${expectedSegmentId}`);
122
+
118
123
  timegroup.currentTimeMs = 0;
124
+ await timegroup.seekTask.taskComplete;
125
+
126
+ // Check what segment actually got loaded
127
+ const actualSegmentId = (jitVideo as any).videoSegmentIdTask.value;
128
+ console.log(`videoSegmentIdTask.value = ${actualSegmentId}`);
129
+
119
130
  const frame = await (jitVideo as any).videoSeekTask.taskComplete;
131
+ console.log(`Frame timestamp when seeking to 0ms: ${frame?.timestamp}`);
132
+
120
133
  expect(frame).toBeDefined();
121
134
  expect(frame?.timestamp).toEqual(0);
122
135
  });
@@ -126,11 +139,11 @@ describe("JIT Media Engine", () => {
126
139
  jitVideo,
127
140
  expect,
128
141
  }) => {
142
+ await timegroup.waitForMediaDurations();
129
143
  timegroup.currentTimeMs = 3_000;
130
- jitVideo.desiredSeekTimeMs = 3_000;
131
- const frame = await (jitVideo as any).videoSeekTask.taskComplete;
132
- expect(frame).toBeDefined();
133
- expect(frame?.timestamp).toEqual(2.96); // Updated: improved mediabunny processing changed frame timing
144
+ await timegroup.seekTask.taskComplete;
145
+ const frame = await jitVideo.videoSeekTask.taskComplete;
146
+ expect(frame?.timestamp).toBeCloseTo(3, 1);
134
147
  });
135
148
 
136
149
  test("seeks to 5 seconds and loads frame", async ({
@@ -138,11 +151,11 @@ describe("JIT Media Engine", () => {
138
151
  jitVideo,
139
152
  expect,
140
153
  }) => {
154
+ await timegroup.waitForMediaDurations();
141
155
  timegroup.currentTimeMs = 5_000;
142
- jitVideo.desiredSeekTimeMs = 5_000;
143
- const frame = await (jitVideo as any).videoSeekTask.taskComplete;
144
- expect(frame).toBeDefined();
145
- expect(frame?.timestamp).toEqual(4.96); // Updated: improved mediabunny processing changed frame timing
156
+ await timegroup.seekTask.taskComplete;
157
+ const frame = await jitVideo.videoSeekTask.taskComplete;
158
+ expect(frame?.timestamp).toBeCloseTo(5, 1);
146
159
  });
147
160
 
148
161
  test("seeks ahead in 50ms increments", async ({
@@ -150,48 +163,47 @@ describe("JIT Media Engine", () => {
150
163
  jitVideo,
151
164
  expect,
152
165
  }) => {
166
+ await timegroup.waitForMediaDurations();
153
167
  timegroup.currentTimeMs = 0;
154
168
  let frame: VideoSample | undefined;
155
169
  for (let i = 0; i <= 3000; i += 50) {
156
170
  timegroup.currentTimeMs = i;
157
- jitVideo.desiredSeekTimeMs = i;
158
- frame = await (jitVideo as any).videoSeekTask.taskComplete;
171
+ await timegroup.seekTask.taskComplete;
172
+ frame = await jitVideo.videoSeekTask.taskComplete;
159
173
  expect(frame).toBeDefined();
160
174
  }
161
- expect(frame?.timestamp).toEqual(0); // Updated: improved mediabunny processing changed frame timing
175
+ expect(frame?.timestamp).toBeCloseTo(3, 1);
162
176
  });
163
177
  });
164
178
 
165
179
  describe("boundary seeking", () => {
166
- // test("segment 2 track range and segment 3 track range have no gap between them", async ({ expect, jitVideo, timegroup }) => {
167
- // // timegroup.contextProvider.currentTimeMs = 0
168
- // timegroup.currentTimeMs = 1000
169
- // jitVideo.desiredSeekTimeMs = 1000;
170
- // await jitVideo.videoSeekTask.taskComplete
171
- // const segment2 = await jitVideo.audioInputTask.taskComplete
172
- // const segment2Audio = await segment2.getFirstAudioTrack();
173
- // const start2 = await segment2Audio?.getFirstTimestamp()
174
- // const end2 = await segment2Audio?.computeDuration()
175
- // const segmentId2 = await jitVideo.audioSegmentIdTask.taskComplete
176
- // console.log({ segmentId2, start2, end2 })
177
-
178
- // timegroup.currentTimeMs = 2.0266666666666664 * 1000
179
- // jitVideo.desiredSeekTimeMs = 2.0266666666666664 * 1000
180
- // await jitVideo.videoSeekTask.taskComplete
181
- // const segment3 = await jitVideo.audioInputTask.taskComplete;
182
- // const segment3Audio = await segment3.getFirstAudioTrack()
183
- // const start3 = await segment3Audio?.getFirstTimestamp();
184
- // const end3 = await segment3Audio?.computeDuration();
185
- // const segmentId3 = await jitVideo.audioSegmentIdTask.taskComplete;
186
- // console.log({ segmentId3, start3, end3 })
187
- // await expect(jitVideo.videoSegmentIdTask.taskComplete).resolves.toBe(2);
188
- // });
180
+ test.skip("segment 2 track range and segment 3 track range have no gap between them", async ({
181
+ expect,
182
+ jitVideo,
183
+ timegroup,
184
+ }) => {
185
+ // SKIP: audioSeekTask is not part of the audio rendering pipeline
186
+ await timegroup.waitForMediaDurations();
187
+ timegroup.currentTimeMs = 1000;
188
+ await timegroup.frameTask.taskComplete;
189
189
 
190
- // test("Can seek audio to 4025.0000000000005ms in head-moov-480p.mp4", async ({ expect, jitVideo, timegroup }) => {
191
- // timegroup.currentTimeMs = 2026.6666666666663;
192
- // jitVideo.desiredSeekTimeMs = 2026.6666666666663;
193
- // await expect(jitVideo.audioSeekTask.taskComplete).resolves.to.not.toThrowError();
194
- // });
190
+ timegroup.currentTimeMs = 2026.6666666666663;
191
+ await timegroup.frameTask.taskComplete;
192
+ const sample = await jitVideo.videoSeekTask.taskComplete;
193
+ expect(sample?.timestamp).toBeCloseTo(2, 1);
194
+ });
195
+
196
+ test("Can seek audio to 4025.0000000000005ms in head-moov-480p.mp4", async ({
197
+ expect,
198
+ jitVideo,
199
+ timegroup,
200
+ }) => {
201
+ await timegroup.waitForMediaDurations();
202
+ timegroup.currentTimeMs = 2026.6666666666663;
203
+ await expect(
204
+ jitVideo.audioSeekTask.taskComplete,
205
+ ).resolves.to.not.toThrowError();
206
+ });
195
207
 
196
208
  test("can seek audio to 4050ms in head-moov-480p.mp4", async ({
197
209
  expect,
@@ -213,20 +225,20 @@ describe("JIT Media Engine", () => {
213
225
  });
214
226
 
215
227
  describe("EFMedia", () => {
216
- // beforeEach(() => {
217
- // // Clean up DOM
218
- // while (document.body.children.length) {
219
- // document.body.children[0]?.remove();
220
- // }
221
- // });
228
+ beforeEach(() => {
229
+ // Clean up DOM
230
+ while (document.body.children.length) {
231
+ document.body.children[0]?.remove();
232
+ }
233
+ });
222
234
 
223
- // afterEach(() => {
224
- // // Clean up any remaining elements
225
- // const elements = document.querySelectorAll("test-media");
226
- // for (const element of elements) {
227
- // element.remove();
228
- // }
229
- // });
235
+ afterEach(() => {
236
+ // Clean up any remaining elements
237
+ const elements = document.querySelectorAll("test-media");
238
+ for (const element of elements) {
239
+ element.remove();
240
+ }
241
+ });
230
242
 
231
243
  const test = baseTest.extend<{
232
244
  element: TestMedia;
@@ -348,12 +348,17 @@ describe("startTimeMs", () => {
348
348
  describe("setting currentTime", () => {
349
349
  test("persists in localStorage if the timegroup has an id and is in the dom", async () => {
350
350
  const timegroup = renderTimegroup(
351
- html`<ef-timegroup id="root" mode="fixed" duration="10s"></ef-timegroup>`,
351
+ html`<ef-timegroup id="localStorage-test" mode="fixed" duration="10s"></ef-timegroup>`,
352
352
  );
353
+ localStorage.removeItem(timegroup.storageKey);
353
354
  document.body.appendChild(timegroup);
354
- assert.isNull(localStorage.getItem(timegroup.storageKey));
355
- timegroup.currentTime = 5_000;
356
- assert.equal(localStorage.getItem(timegroup.storageKey), "10"); // Clamped to duration
355
+ await timegroup.waitForMediaDurations();
356
+
357
+ timegroup.currentTime = 5_000; // 5000 seconds, should clamp to 10s
358
+ await timegroup.seekTask.taskComplete;
359
+
360
+ const storedValue = localStorage.getItem(timegroup.storageKey);
361
+ assert.equal(storedValue, "10"); // Should store 10 (clamped from 5000 to duration)
357
362
  timegroup.remove();
358
363
  });
359
364
 
@@ -52,10 +52,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
52
52
 
53
53
  #currentTime = 0;
54
54
 
55
- // Frame update locking mechanism (only for root timegroups)
56
- private isFrameUpdateInProgress = false;
57
- private queuedTimeUpdate: number | null = null;
58
-
59
55
  @property({
60
56
  type: String,
61
57
  attribute: "mode",
@@ -74,24 +70,33 @@ export class EFTimegroup extends EFTemporal(LitElement) {
74
70
 
75
71
  #resizeObserver?: ResizeObserver;
76
72
 
73
+ #seekInProgress = false;
74
+
75
+ #pendingSeekTime: number | undefined;
76
+
77
77
  @property({ type: Number, attribute: "currenttime" })
78
78
  set currentTime(time: number) {
79
- const newTime = Math.max(0, Math.min(time, this.durationMs / 1000));
80
-
81
- // Only apply locking mechanism for root timegroups to prevent cascade overload
82
- if (this.isRootTimegroup && this.isFrameUpdateInProgress) {
83
- // Queue the latest time update - only keep the most recent
84
- this.queuedTimeUpdate = newTime;
79
+ if (this.#seekInProgress) {
80
+ this.#pendingSeekTime = time;
85
81
  return;
86
82
  }
87
83
 
88
- if (this.isRootTimegroup) {
89
- this.#executeTimeUpdate(newTime);
90
- } else {
91
- // Non-root timegroups update immediately (no cascade risk)
92
- this.#currentTime = newTime;
93
- this.#saveTimeToLocalStorage(newTime);
94
- }
84
+ this.#seekInProgress = true;
85
+ this.#pendingSeekTime = time;
86
+
87
+ this.seekTask.run().finally(() => {
88
+ this.#seekInProgress = false;
89
+ if (
90
+ this.#pendingSeekTime !== undefined &&
91
+ this.#pendingSeekTime !== time
92
+ ) {
93
+ const pendingTime = this.#pendingSeekTime;
94
+ this.#pendingSeekTime = undefined;
95
+ this.currentTime = pendingTime;
96
+ } else {
97
+ this.#pendingSeekTime = undefined;
98
+ }
99
+ });
95
100
  }
96
101
 
97
102
  get currentTime() {
@@ -113,34 +118,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
113
118
  return this.closest("ef-timegroup") === this;
114
119
  }
115
120
 
116
- /**
117
- * Executes time update with frame locking for root timegroups
118
- */
119
- async #executeTimeUpdate(time: number) {
120
- this.isFrameUpdateInProgress = true;
121
- this.#currentTime = time;
122
-
123
- try {
124
- // Save to localStorage
125
- this.#saveTimeToLocalStorage(time);
126
-
127
- // Wait for any pending frame tasks to complete before allowing next update
128
- await this.waitForFrameTasks();
129
- } catch (error) {
130
- console.error("⚠️ [TIME_UPDATE_ERROR] Error during frame update:", error);
131
- } finally {
132
- this.isFrameUpdateInProgress = false;
133
-
134
- // Process queued update if any (ensures latest scrub position is processed)
135
- if (this.queuedTimeUpdate !== null && this.queuedTimeUpdate !== time) {
136
- const nextTime = this.queuedTimeUpdate;
137
- this.queuedTimeUpdate = null;
138
- // Schedule on next tick to avoid recursive call stack
139
- setTimeout(() => this.#executeTimeUpdate(nextTime), 0);
140
- }
141
- }
142
- }
143
-
144
121
  /**
145
122
  * Saves time to localStorage (extracted for reuse)
146
123
  */
@@ -260,24 +237,45 @@ export class EFTimegroup extends EFTemporal(LitElement) {
260
237
  }
261
238
  }
262
239
 
263
- async getPendingFrameTasks() {
264
- await this.updateComplete;
240
+ async getPendingFrameTasks(signal?: AbortSignal) {
241
+ await this.waitForNestedUpdates(signal);
242
+ signal?.throwIfAborted();
265
243
  const temporals = deepGetElementsWithFrameTasks(this);
266
244
  return temporals
267
245
  .map((temporal) => temporal.frameTask)
268
246
  .filter((task) => task.status < TaskStatus.COMPLETE);
269
247
  }
270
248
 
271
- async waitForFrameTasks() {
249
+ async waitForNestedUpdates(signal?: AbortSignal) {
250
+ const limit = 10;
251
+ let steps = 0;
252
+ let isComplete = true;
253
+ while (true) {
254
+ steps++;
255
+ if (steps > limit) {
256
+ throw new Error("Reached update depth limit.");
257
+ }
258
+ isComplete = await this.updateComplete;
259
+ signal?.throwIfAborted();
260
+ if (isComplete) {
261
+ break;
262
+ }
263
+ }
264
+ }
265
+
266
+ async waitForFrameTasks(signal?: AbortSignal) {
272
267
  const limit = 10;
273
268
  let step = 0;
274
- await this.updateComplete;
269
+ await this.waitForNestedUpdates(signal);
275
270
  while (step < limit) {
276
271
  step++;
277
- let pendingTasks = await this.getPendingFrameTasks();
272
+ let pendingTasks = await this.getPendingFrameTasks(signal);
273
+ signal?.throwIfAborted();
278
274
  await Promise.all(pendingTasks.map((task) => task.taskComplete));
275
+ signal?.throwIfAborted();
279
276
  await this.updateComplete;
280
- pendingTasks = await this.getPendingFrameTasks();
277
+ signal?.throwIfAborted();
278
+ pendingTasks = await this.getPendingFrameTasks(signal);
281
279
  if (pendingTasks.length === 0) {
282
280
  break;
283
281
  }
@@ -549,13 +547,39 @@ export class EFTimegroup extends EFTemporal(LitElement) {
549
547
  frameTask = new Task(this, {
550
548
  autoRun: EF_INTERACTIVE,
551
549
  args: () => [this.ownCurrentTimeMs, this.currentTimeMs] as const,
552
- task: async ([], { signal: _signal }) => {
553
- let fullyUpdated = await this.updateComplete;
554
- while (!fullyUpdated) {
555
- fullyUpdated = await this.updateComplete;
550
+ task: async ([], { signal }) => {
551
+ if (this.isRootTimegroup) {
552
+ await this.waitForFrameTasks(signal);
556
553
  }
557
554
  },
558
555
  });
556
+
557
+ seekTask = new Task(this, {
558
+ args: () => [this.#pendingSeekTime ?? this.#currentTime] as const,
559
+ task: async ([targetTime], { signal }) => {
560
+ const newTime = Math.max(0, Math.min(targetTime, this.durationMs / 1000));
561
+ this.#currentTime = newTime;
562
+ this.requestUpdate("currentTime");
563
+
564
+ // Wait for update to propagate to child elements
565
+ await this.updateComplete;
566
+ signal.throwIfAborted();
567
+
568
+ // Trigger child video seek tasks since they don't auto-run anymore
569
+ const videoElements = this.querySelectorAll(
570
+ "ef-video",
571
+ ) as NodeListOf<any>;
572
+ for (const video of videoElements) {
573
+ if (video.videoSeekTask) {
574
+ video.videoSeekTask.run();
575
+ }
576
+ }
577
+
578
+ // Run frame task and wait for completion
579
+ await this.frameTask.run();
580
+ this.#saveTimeToLocalStorage(newTime);
581
+ },
582
+ });
559
583
  }
560
584
 
561
585
  declare global {