@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
@@ -1,7 +1,7 @@
1
1
  import { provide } from "@lit/context";
2
2
  import { Task, TaskStatus } from "@lit/task";
3
3
  import debug from "debug";
4
- import { css, html, LitElement, type PropertyValueMap } from "lit";
4
+ import { css, html, LitElement } from "lit";
5
5
  import { customElement, property } from "lit/decorators.js";
6
6
 
7
7
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
@@ -12,14 +12,22 @@ import {
12
12
  deepGetElementsWithFrameTasks,
13
13
  EFTemporal,
14
14
  flushStartTimeMsCache,
15
+ resetTemporalCache,
15
16
  shallowGetTemporalElements,
16
17
  timegroupContext,
17
18
  } from "./EFTemporal.js";
18
19
  import { TimegroupController } from "./TimegroupController.js";
19
- import { updateAnimations } from "./updateAnimations.ts";
20
+ import { evaluateTemporalState, updateAnimations } from "./updateAnimations.ts";
20
21
 
21
22
  const log = debug("ef:elements:EFTimegroup");
22
23
 
24
+ // Cache for sequence mode duration calculations to avoid O(n) recalculation
25
+ let sequenceDurationCache: WeakMap<EFTimegroup, number> = new WeakMap();
26
+
27
+ export const flushSequenceDurationCache = () => {
28
+ sequenceDurationCache = new WeakMap();
29
+ };
30
+
23
31
  export const shallowGetTimegroups = (
24
32
  element: Element,
25
33
  groups: EFTimegroup[] = [],
@@ -50,20 +58,40 @@ export class EFTimegroup extends EFTemporal(LitElement) {
50
58
  @provide({ context: timegroupContext })
51
59
  _timeGroupContext = this;
52
60
 
53
- #currentTime = 0;
61
+ #currentTime: number | undefined = undefined;
54
62
 
55
63
  @property({
56
64
  type: String,
57
65
  attribute: "mode",
58
66
  })
59
- mode: "fit" | "fixed" | "sequence" | "contain" = "contain";
67
+ set mode(value: "fit" | "fixed" | "sequence" | "contain") {
68
+ // Invalidate duration cache when mode changes
69
+ sequenceDurationCache.delete(this);
70
+ this._mode = value;
71
+ }
72
+
73
+ get mode() {
74
+ return this._mode;
75
+ }
76
+
77
+ private _mode: "fit" | "fixed" | "sequence" | "contain" = "contain";
60
78
 
61
79
  @property({
62
80
  type: Number,
63
81
  converter: durationConverter,
64
82
  attribute: "overlap",
65
83
  })
66
- overlapMs = 0;
84
+ set overlapMs(value: number) {
85
+ // Invalidate duration cache when overlap changes
86
+ sequenceDurationCache.delete(this);
87
+ this._overlapMs = value;
88
+ }
89
+
90
+ get overlapMs() {
91
+ return this._overlapMs;
92
+ }
93
+
94
+ private _overlapMs = 0;
67
95
 
68
96
  @property({ type: String })
69
97
  fit: "none" | "contain" | "cover" = "none";
@@ -74,25 +102,47 @@ export class EFTimegroup extends EFTemporal(LitElement) {
74
102
 
75
103
  #pendingSeekTime: number | undefined;
76
104
 
105
+ #processingPendingSeek = false;
106
+
77
107
  @property({ type: Number, attribute: "currenttime" })
78
108
  set currentTime(time: number) {
109
+ time = Math.max(0, time);
110
+ if (!this.isRootTimegroup) {
111
+ return;
112
+ }
113
+ if (Number.isNaN(time)) {
114
+ return;
115
+ }
116
+ if (time === this.#currentTime && !this.#processingPendingSeek) {
117
+ return;
118
+ }
119
+ if (this.#pendingSeekTime === time) {
120
+ return;
121
+ }
122
+
79
123
  if (this.#seekInProgress) {
80
124
  this.#pendingSeekTime = time;
125
+ this.#currentTime = time;
81
126
  return;
82
127
  }
83
128
 
129
+ this.#currentTime = time;
130
+ // This will be set to false in the seekTask
84
131
  this.#seekInProgress = true;
85
- this.#pendingSeekTime = time;
86
132
 
87
133
  this.seekTask.run().finally(() => {
88
- this.#seekInProgress = false;
89
134
  if (
90
135
  this.#pendingSeekTime !== undefined &&
91
136
  this.#pendingSeekTime !== time
92
137
  ) {
93
138
  const pendingTime = this.#pendingSeekTime;
94
139
  this.#pendingSeekTime = undefined;
95
- this.currentTime = pendingTime;
140
+ this.#processingPendingSeek = true;
141
+ try {
142
+ this.currentTime = pendingTime;
143
+ } finally {
144
+ this.#processingPendingSeek = false;
145
+ }
96
146
  } else {
97
147
  this.#pendingSeekTime = undefined;
98
148
  }
@@ -100,22 +150,22 @@ export class EFTimegroup extends EFTemporal(LitElement) {
100
150
  }
101
151
 
102
152
  get currentTime() {
103
- return this.#currentTime;
104
- }
105
-
106
- get currentTimeMs() {
107
- return this.currentTime * 1000;
153
+ return this.#currentTime ?? 0;
108
154
  }
109
155
 
110
156
  set currentTimeMs(ms: number) {
111
157
  this.currentTime = ms / 1000;
112
158
  }
113
159
 
160
+ get currentTimeMs() {
161
+ return this.currentTime * 1000;
162
+ }
163
+
114
164
  /**
115
165
  * Determines if this is a root timegroup (no parent timegroups)
116
166
  */
117
167
  get isRootTimegroup(): boolean {
118
- return this.closest("ef-timegroup") === this;
168
+ return !this.parentTimegroup;
119
169
  }
120
170
 
121
171
  /**
@@ -123,7 +173,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
123
173
  */
124
174
  #saveTimeToLocalStorage(time: number) {
125
175
  try {
126
- if (this.id && this.isConnected) {
176
+ if (this.id && this.isConnected && !Number.isNaN(time)) {
127
177
  localStorage.setItem(this.storageKey, time.toString());
128
178
  }
129
179
  } catch (error) {
@@ -132,27 +182,46 @@ export class EFTimegroup extends EFTemporal(LitElement) {
132
182
  }
133
183
 
134
184
  render() {
135
- return html`<slot></slot> `;
185
+ return html`<slot @slotchange=${this.#handleSlotChange}></slot> `;
136
186
  }
137
187
 
188
+ #handleSlotChange = () => {
189
+ // Invalidate caches when slot content changes
190
+ resetTemporalCache();
191
+ flushSequenceDurationCache();
192
+ flushStartTimeMsCache();
193
+
194
+ // Request update to trigger recalculation of dependent properties
195
+ this.requestUpdate();
196
+ };
197
+
138
198
  maybeLoadTimeFromLocalStorage() {
139
199
  if (this.id) {
140
200
  try {
141
- return Number.parseFloat(localStorage.getItem(this.storageKey) || "0");
201
+ const storedValue = localStorage.getItem(this.storageKey);
202
+ if (storedValue === null) {
203
+ return undefined;
204
+ }
205
+ return Number.parseFloat(storedValue);
142
206
  } catch (error) {
143
207
  log("Failed to load time from localStorage", error);
144
208
  }
145
209
  }
146
- return 0;
147
210
  }
148
211
 
149
212
  connectedCallback() {
150
213
  super.connectedCallback();
151
- if (this.id) {
152
- this.waitForMediaDurations().then(() => {
153
- this.currentTime = this.maybeLoadTimeFromLocalStorage();
154
- });
155
- }
214
+ this.waitForMediaDurations().then(() => {
215
+ if (this.id) {
216
+ const maybeLoadedTime = this.maybeLoadTimeFromLocalStorage();
217
+ if (maybeLoadedTime !== undefined) {
218
+ this.currentTime = maybeLoadedTime;
219
+ }
220
+ }
221
+ if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) {
222
+ this.seekTask.run();
223
+ }
224
+ });
156
225
 
157
226
  if (this.parentTimegroup) {
158
227
  new TimegroupController(this.parentTimegroup, this);
@@ -161,10 +230,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
161
230
  if (this.shouldWrapWithWorkbench()) {
162
231
  this.wrapWithWorkbench();
163
232
  }
164
-
165
- requestAnimationFrame(() => {
166
- this.updateAnimations();
167
- });
168
233
  }
169
234
 
170
235
  disconnectedCallback() {
@@ -205,6 +270,12 @@ export class EFTimegroup extends EFTemporal(LitElement) {
205
270
  case "fixed":
206
271
  return super.durationMs;
207
272
  case "sequence": {
273
+ // Check cache first to avoid expensive O(n) recalculation
274
+ const cachedDuration = sequenceDurationCache.get(this);
275
+ if (cachedDuration !== undefined) {
276
+ return cachedDuration;
277
+ }
278
+
208
279
  let duration = 0;
209
280
  this.childTemporals.forEach((child, index) => {
210
281
  if (child instanceof EFTimegroup && child.mode === "fit") {
@@ -215,6 +286,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
215
286
  }
216
287
  duration += child.durationMs;
217
288
  });
289
+
290
+ // Cache the calculated duration
291
+ sequenceDurationCache.set(this, duration);
218
292
  return duration;
219
293
  }
220
294
  case "contain": {
@@ -241,9 +315,34 @@ export class EFTimegroup extends EFTemporal(LitElement) {
241
315
  await this.waitForNestedUpdates(signal);
242
316
  signal?.throwIfAborted();
243
317
  const temporals = deepGetElementsWithFrameTasks(this);
244
- return temporals
245
- .map((temporal) => temporal.frameTask)
246
- .filter((task) => task.status < TaskStatus.COMPLETE);
318
+
319
+ // Filter to only include temporally visible elements for frame processing
320
+ // (but keep all elements for duration calculations)
321
+ // Use the target timeline time if we're in the middle of seeking
322
+ const timelineTimeMs =
323
+ (this.#pendingSeekTime ?? this.#currentTime ?? 0) * 1000;
324
+ const activeTemporals = temporals.filter((temporal) => {
325
+ // Skip timeline filtering if temporal doesn't have timeline position info
326
+ if (!("startTimeMs" in temporal) || !("endTimeMs" in temporal)) {
327
+ return true; // Keep non-temporal elements
328
+ }
329
+
330
+ // Only process frame tasks for elements that overlap the current timeline
331
+ // Use same epsilon logic as seek task for consistency
332
+ const epsilon = 0.001; // 1µs offset to break ties at boundaries
333
+ const startTimeMs = (temporal as any).startTimeMs as number;
334
+ const endTimeMs = (temporal as any).endTimeMs as number;
335
+ const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
336
+ const elementEndsAfterStart = endTimeMs > timelineTimeMs; // Exclusive end for clean transitions
337
+ return elementStartsBeforeEnd && elementEndsAfterStart;
338
+ });
339
+
340
+ const frameTasks = activeTemporals.map((temporal) => temporal.frameTask);
341
+ frameTasks.forEach((task) => {
342
+ task.run();
343
+ });
344
+
345
+ return frameTasks.filter((task) => task.status < TaskStatus.COMPLETE);
247
346
  }
248
347
 
249
348
  async waitForNestedUpdates(signal?: AbortSignal) {
@@ -263,23 +362,29 @@ export class EFTimegroup extends EFTemporal(LitElement) {
263
362
  }
264
363
  }
265
364
 
266
- async waitForFrameTasks(signal?: AbortSignal) {
267
- const limit = 10;
268
- let step = 0;
269
- await this.waitForNestedUpdates(signal);
270
- while (step < limit) {
271
- step++;
272
- let pendingTasks = await this.getPendingFrameTasks(signal);
273
- signal?.throwIfAborted();
274
- await Promise.all(pendingTasks.map((task) => task.taskComplete));
275
- signal?.throwIfAborted();
276
- await this.updateComplete;
277
- signal?.throwIfAborted();
278
- pendingTasks = await this.getPendingFrameTasks(signal);
279
- if (pendingTasks.length === 0) {
280
- break;
281
- }
365
+ async waitForFrameTasks() {
366
+ const temporalElements = deepGetElementsWithFrameTasks(this);
367
+
368
+ // Filter to only include temporally visible elements for frame processing
369
+ const visibleElements = temporalElements.filter((element) => {
370
+ const temporalState = evaluateTemporalState(element);
371
+ return temporalState.isVisible;
372
+ });
373
+
374
+ await Promise.all(
375
+ visibleElements.map((element) => {
376
+ return element.frameTask.run();
377
+ }),
378
+ );
379
+ }
380
+
381
+ mediaDurationsPromise: Promise<void> | undefined = undefined;
382
+
383
+ async waitForMediaDurations() {
384
+ if (!this.mediaDurationsPromise) {
385
+ this.mediaDurationsPromise = this.#waitForMediaDurations();
282
386
  }
387
+ return this.mediaDurationsPromise;
283
388
  }
284
389
 
285
390
  /**
@@ -288,14 +393,18 @@ export class EFTimegroup extends EFTemporal(LitElement) {
288
393
  * that caused issues with constructing audio data. We had negative durations
289
394
  * in calculations and it was not clear why.
290
395
  */
291
- async waitForMediaDurations() {
396
+ async #waitForMediaDurations() {
292
397
  // We must await updateComplete to ensure all media elements inside this are connected
293
398
  // and will match deepGetMediaElements
294
399
  await this.updateComplete;
295
400
  const mediaElements = deepGetMediaElements(this);
296
401
  // Then, we must await the fragmentIndexTask to ensure all media elements have their
297
402
  // fragment index loaded, which is where their duration is parsed from.
298
- await Promise.all(mediaElements.map((m) => m.mediaEngineTask.taskComplete));
403
+ await Promise.all(
404
+ mediaElements.map((m) =>
405
+ m.mediaEngineTask.value ? Promise.resolve() : m.mediaEngineTask.run(),
406
+ ),
407
+ );
299
408
 
300
409
  // After waiting for durations, we must force some updates to cascade and ensure all temporal elements
301
410
  // have correct durations and start times. It is not ideal that we have to do this inside here,
@@ -305,6 +414,9 @@ export class EFTimegroup extends EFTemporal(LitElement) {
305
414
  // startTimeMs parsed fresh, otherwise the startTimeMs is cached per animation frame.
306
415
  flushStartTimeMsCache();
307
416
 
417
+ // Flush duration cache since child durations may have changed
418
+ flushSequenceDurationCache();
419
+
308
420
  // Request an update to the currentTime of this group, ensuring that time updates will cascade
309
421
  // down to children, forcing sequence groups to arrange correctly.
310
422
  // This also makes the filmstrip update correctly.
@@ -319,22 +431,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
319
431
  return shallowGetTemporalElements(this);
320
432
  }
321
433
 
322
- protected updated(
323
- changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
324
- ): void {
325
- super.updated(changedProperties);
326
- if (
327
- changedProperties.has("currentTime") ||
328
- changedProperties.has("ownCurrentTimeMs")
329
- ) {
330
- this.updateAnimations();
331
- }
332
- }
333
-
334
- private updateAnimations() {
335
- updateAnimations(this);
336
- }
337
-
338
434
  get contextProvider() {
339
435
  let parent = this.parentNode;
340
436
  while (parent) {
@@ -424,9 +520,15 @@ export class EFTimegroup extends EFTemporal(LitElement) {
424
520
  return;
425
521
  }
426
522
 
523
+ // Convert from local timeline to source media timeline (accounting for sourcein/sourceout)
524
+ const sourceInMs =
525
+ mediaElement.sourceInMs || mediaElement.trimStartMs || 0;
526
+ const mediaSourceFromMs = mediaLocalFromMs + sourceInMs;
527
+ const mediaSourceToMs = mediaLocalToMs + sourceInMs;
528
+
427
529
  const audio = await mediaElement.fetchAudioSpanningTime(
428
- mediaLocalFromMs, // ✅ Now using media element's local timeline
429
- mediaLocalToMs, // ✅ Now using media element's local timeline
530
+ mediaSourceFromMs, // ✅ Now using source media timeline with sourcein/sourceout
531
+ mediaSourceToMs, // ✅ Now using source media timeline with sourcein/sourceout
430
532
  abortController.signal,
431
533
  );
432
534
  if (!audio) {
@@ -441,24 +543,32 @@ export class EFTimegroup extends EFTemporal(LitElement) {
441
543
 
442
544
  // Calculate timing for placing this audio in the output context
443
545
  const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
444
- const ctxEndMs = mediaElement.endTimeMs - fromMs;
445
- const ctxDurationMs = ctxEndMs - ctxStartMs;
446
546
 
447
547
  // Calculate offset within the fetched audio buffer
448
- // Since we now use local timeline coordinates, audio.startMs is relative to media start
449
- const requestedOffsetInMedia = mediaLocalFromMs; // Already in local timeline
450
- const actualOffsetInBuffer = requestedOffsetInMedia - audio.startMs; // Both in local timeline
548
+ // audio.startMs is now in source timeline, convert back to compare properly
549
+ const requestedSourceFromMs = mediaSourceFromMs;
550
+ const actualSourceStartMs = audio.startMs;
551
+ const offsetInBufferMs = requestedSourceFromMs - actualSourceStartMs;
451
552
 
452
553
  // Ensure offset is never negative (this would cause audio scheduling errors)
453
- const safeOffset = Math.max(0, actualOffsetInBuffer);
554
+ const safeOffsetMs = Math.max(0, offsetInBufferMs);
555
+
556
+ // Calculate exact duration to play from the buffer (don't exceed what we need)
557
+ const requestedDurationMs = mediaSourceToMs - mediaSourceFromMs;
558
+ const availableAudioMs = audio.endMs - audio.startMs;
559
+ const actualDurationMs = Math.min(
560
+ requestedDurationMs,
561
+ availableAudioMs - safeOffsetMs,
562
+ );
454
563
 
455
- if (safeOffset !== actualOffsetInBuffer) {
564
+ if (actualDurationMs <= 0) {
565
+ return; // Skip if no valid audio duration
456
566
  }
457
567
 
458
568
  bufferSource.start(
459
- ctxStartMs / 1000,
460
- safeOffset / 1000,
461
- ctxDurationMs / 1000,
569
+ ctxStartMs / 1000, // When to start in output context (seconds)
570
+ safeOffsetMs / 1000, // Offset into the fetched buffer (seconds)
571
+ actualDurationMs / 1000, // How long to play from buffer (seconds)
462
572
  );
463
573
  }),
464
574
  );
@@ -545,39 +655,36 @@ export class EFTimegroup extends EFTemporal(LitElement) {
545
655
  }
546
656
 
547
657
  frameTask = new Task(this, {
548
- autoRun: EF_INTERACTIVE,
658
+ // autoRun: EF_INTERACTIVE,
659
+ autoRun: false,
549
660
  args: () => [this.ownCurrentTimeMs, this.currentTimeMs] as const,
550
- task: async ([], { signal }) => {
661
+ task: async ([]) => {
551
662
  if (this.isRootTimegroup) {
552
- await this.waitForFrameTasks(signal);
663
+ await this.waitForFrameTasks();
664
+ updateAnimations(this);
553
665
  }
554
666
  },
555
667
  });
556
668
 
557
669
  seekTask = new Task(this, {
670
+ autoRun: false,
558
671
  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
- }
672
+ onComplete: () => {},
673
+ task: async ([targetTime]) => {
674
+ if (!this.isRootTimegroup) {
675
+ return;
576
676
  }
577
-
578
- // Run frame task and wait for completion
677
+ await this.waitForMediaDurations();
678
+ const newTime = Math.max(
679
+ 0,
680
+ Math.min(targetTime ?? 0, this.durationMs / 1000),
681
+ );
682
+ this.requestUpdate("currentTime");
579
683
  await this.frameTask.run();
580
684
  this.#saveTimeToLocalStorage(newTime);
685
+ // This has to be set false here so any following seeks are not treated as pending
686
+ this.#seekInProgress = false;
687
+ return newTime;
581
688
  },
582
689
  });
583
690
  }