@editframe/elements 0.18.20-beta.0 → 0.18.22-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 (41) 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/EFTemporal.d.ts +0 -1
  11. package/dist/elements/EFTemporal.js +4 -8
  12. package/dist/elements/EFTimegroup.d.ts +4 -4
  13. package/dist/elements/EFTimegroup.js +52 -60
  14. package/dist/elements/EFVideo.d.ts +1 -32
  15. package/dist/elements/EFVideo.js +13 -51
  16. package/dist/elements/SampleBuffer.js +1 -1
  17. package/dist/gui/ContextMixin.browsertest.d.ts +1 -1
  18. package/dist/gui/ContextMixin.js +1 -1
  19. package/package.json +2 -2
  20. package/src/elements/EFAudio.browsertest.ts +0 -3
  21. package/src/elements/EFAudio.ts +3 -22
  22. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +39 -1
  23. package/src/elements/EFMedia/AssetMediaEngine.ts +5 -4
  24. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +90 -185
  25. package/src/elements/EFMedia/BufferedSeekingInput.ts +119 -130
  26. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +21 -21
  27. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +10 -5
  28. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +33 -34
  29. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +22 -20
  30. package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +0 -3
  31. package/src/elements/EFMedia.browsertest.ts +72 -60
  32. package/src/elements/EFTemporal.ts +5 -15
  33. package/src/elements/EFTimegroup.browsertest.ts +9 -4
  34. package/src/elements/EFTimegroup.ts +79 -95
  35. package/src/elements/EFVideo.browsertest.ts +172 -160
  36. package/src/elements/EFVideo.ts +17 -73
  37. package/src/elements/SampleBuffer.ts +1 -2
  38. package/src/gui/ContextMixin.browsertest.ts +5 -2
  39. package/src/gui/ContextMixin.ts +7 -0
  40. package/test/EFVideo.framegen.browsertest.ts +0 -54
  41. 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;
@@ -236,32 +236,22 @@ export const deepGetElementsWithFrameTasks = (
236
236
  };
237
237
 
238
238
  let temporalCache: Map<Element, TemporalMixinInterface[]>;
239
- let modifiedElements = new WeakSet<Element>();
240
-
241
239
  const resetTemporalCache = () => {
242
240
  temporalCache = new Map();
243
- modifiedElements = new WeakSet();
244
241
  if (typeof requestAnimationFrame !== "undefined") {
245
242
  requestAnimationFrame(resetTemporalCache);
246
243
  }
247
244
  };
248
245
  resetTemporalCache();
249
246
 
250
- export const clearTemporalCacheForElement = (element: Element) => {
251
- temporalCache.delete(element);
252
- modifiedElements.add(element);
253
- };
254
-
255
247
  export const shallowGetTemporalElements = (
256
248
  element: Element,
257
249
  temporals: TemporalMixinInterface[] = [],
258
250
  ) => {
259
- // Temporarily disable caching to ensure reactivity works correctly
260
- // TODO: Implement proper cache invalidation mechanism
261
-
262
- // Clear the temporals array to ensure fresh results
263
- temporals.length = 0;
264
-
251
+ const cachedResult = temporalCache.get(element);
252
+ if (cachedResult) {
253
+ return cachedResult;
254
+ }
265
255
  for (const child of element.children) {
266
256
  if (isEFTemporal(child)) {
267
257
  temporals.push(child);
@@ -269,7 +259,7 @@ export const shallowGetTemporalElements = (
269
259
  shallowGetTemporalElements(child, temporals);
270
260
  }
271
261
  }
272
-
262
+ temporalCache.set(element, temporals);
273
263
  return temporals;
274
264
  };
275
265
 
@@ -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",
@@ -73,26 +69,34 @@ export class EFTimegroup extends EFTemporal(LitElement) {
73
69
  fit: "none" | "contain" | "cover" = "none";
74
70
 
75
71
  #resizeObserver?: ResizeObserver;
76
- #childObserver?: MutationObserver;
72
+
73
+ #seekInProgress = false;
74
+
75
+ #pendingSeekTime: number | undefined;
77
76
 
78
77
  @property({ type: Number, attribute: "currenttime" })
79
78
  set currentTime(time: number) {
80
- const newTime = Math.max(0, Math.min(time, this.durationMs / 1000));
81
-
82
- // Only apply locking mechanism for root timegroups to prevent cascade overload
83
- if (this.isRootTimegroup && this.isFrameUpdateInProgress) {
84
- // Queue the latest time update - only keep the most recent
85
- this.queuedTimeUpdate = newTime;
79
+ if (this.#seekInProgress) {
80
+ this.#pendingSeekTime = time;
86
81
  return;
87
82
  }
88
83
 
89
- if (this.isRootTimegroup) {
90
- this.#executeTimeUpdate(newTime);
91
- } else {
92
- // Non-root timegroups update immediately (no cascade risk)
93
- this.#currentTime = newTime;
94
- this.#saveTimeToLocalStorage(newTime);
95
- }
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
+ });
96
100
  }
97
101
 
98
102
  get currentTime() {
@@ -114,34 +118,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
114
118
  return this.closest("ef-timegroup") === this;
115
119
  }
116
120
 
117
- /**
118
- * Executes time update with frame locking for root timegroups
119
- */
120
- async #executeTimeUpdate(time: number) {
121
- this.isFrameUpdateInProgress = true;
122
- this.#currentTime = time;
123
-
124
- try {
125
- // Save to localStorage
126
- this.#saveTimeToLocalStorage(time);
127
-
128
- // Wait for any pending frame tasks to complete before allowing next update
129
- await this.waitForFrameTasks();
130
- } catch (error) {
131
- console.error("⚠️ [TIME_UPDATE_ERROR] Error during frame update:", error);
132
- } finally {
133
- this.isFrameUpdateInProgress = false;
134
-
135
- // Process queued update if any (ensures latest scrub position is processed)
136
- if (this.queuedTimeUpdate !== null && this.queuedTimeUpdate !== time) {
137
- const nextTime = this.queuedTimeUpdate;
138
- this.queuedTimeUpdate = null;
139
- // Schedule on next tick to avoid recursive call stack
140
- setTimeout(() => this.#executeTimeUpdate(nextTime), 0);
141
- }
142
- }
143
- }
144
-
145
121
  /**
146
122
  * Saves time to localStorage (extracted for reuse)
147
123
  */
@@ -186,44 +162,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
186
162
  this.wrapWithWorkbench();
187
163
  }
188
164
 
189
- // Set up observer to detect child changes that affect duration
190
- this.#childObserver = new MutationObserver((mutations) => {
191
- let shouldUpdate = false;
192
-
193
- for (const mutation of mutations) {
194
- if (mutation.type === "childList") {
195
- // Child added/removed - this affects duration for contain/sequence modes
196
- shouldUpdate = true;
197
- } else if (mutation.type === "attributes") {
198
- // Attribute changes that might affect duration
199
- if (
200
- mutation.attributeName === "duration" ||
201
- mutation.attributeName === "mode"
202
- ) {
203
- shouldUpdate = true;
204
- }
205
- }
206
- }
207
-
208
- if (shouldUpdate) {
209
- // Clear the temporal cache for this element to ensure childTemporals is up to date
210
- import("./EFTemporal.js").then(({ clearTemporalCacheForElement }) => {
211
- clearTemporalCacheForElement(this);
212
- });
213
-
214
- // Trigger an update to recalculate computed properties
215
- this.requestUpdate();
216
- }
217
- });
218
-
219
- // Observe this element for child changes
220
- this.#childObserver.observe(this, {
221
- childList: true,
222
- subtree: true,
223
- attributes: true,
224
- attributeFilter: ["duration", "mode"],
225
- });
226
-
227
165
  requestAnimationFrame(() => {
228
166
  this.updateAnimations();
229
167
  });
@@ -232,7 +170,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
232
170
  disconnectedCallback() {
233
171
  super.disconnectedCallback();
234
172
  this.#resizeObserver?.disconnect();
235
- this.#childObserver?.disconnect();
236
173
  }
237
174
 
238
175
  get storageKey() {
@@ -300,24 +237,45 @@ export class EFTimegroup extends EFTemporal(LitElement) {
300
237
  }
301
238
  }
302
239
 
303
- async getPendingFrameTasks() {
304
- await this.updateComplete;
240
+ async getPendingFrameTasks(signal?: AbortSignal) {
241
+ await this.waitForNestedUpdates(signal);
242
+ signal?.throwIfAborted();
305
243
  const temporals = deepGetElementsWithFrameTasks(this);
306
244
  return temporals
307
245
  .map((temporal) => temporal.frameTask)
308
246
  .filter((task) => task.status < TaskStatus.COMPLETE);
309
247
  }
310
248
 
311
- 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) {
312
267
  const limit = 10;
313
268
  let step = 0;
314
- await this.updateComplete;
269
+ await this.waitForNestedUpdates(signal);
315
270
  while (step < limit) {
316
271
  step++;
317
- let pendingTasks = await this.getPendingFrameTasks();
272
+ let pendingTasks = await this.getPendingFrameTasks(signal);
273
+ signal?.throwIfAborted();
318
274
  await Promise.all(pendingTasks.map((task) => task.taskComplete));
275
+ signal?.throwIfAborted();
319
276
  await this.updateComplete;
320
- pendingTasks = await this.getPendingFrameTasks();
277
+ signal?.throwIfAborted();
278
+ pendingTasks = await this.getPendingFrameTasks(signal);
321
279
  if (pendingTasks.length === 0) {
322
280
  break;
323
281
  }
@@ -589,11 +547,37 @@ export class EFTimegroup extends EFTemporal(LitElement) {
589
547
  frameTask = new Task(this, {
590
548
  autoRun: EF_INTERACTIVE,
591
549
  args: () => [this.ownCurrentTimeMs, this.currentTimeMs] as const,
592
- task: async ([], { signal: _signal }) => {
593
- let fullyUpdated = await this.updateComplete;
594
- while (!fullyUpdated) {
595
- fullyUpdated = await this.updateComplete;
550
+ task: async ([], { signal }) => {
551
+ if (this.isRootTimegroup) {
552
+ await this.waitForFrameTasks(signal);
553
+ }
554
+ },
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
+ }
596
576
  }
577
+
578
+ // Run frame task and wait for completion
579
+ await this.frameTask.run();
580
+ this.#saveTimeToLocalStorage(newTime);
597
581
  },
598
582
  });
599
583
  }