@editframe/elements 0.18.26-beta.0 → 0.19.2-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 (75) 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/BaseMediaEngine.js +1 -5
  4. package/dist/elements/EFMedia/JitMediaEngine.d.ts +10 -0
  5. package/dist/elements/EFMedia/JitMediaEngine.js +12 -0
  6. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +16 -12
  7. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.d.ts +1 -1
  8. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +0 -4
  9. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +1 -1
  10. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +0 -4
  11. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +1 -1
  12. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +3 -2
  13. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +16 -12
  14. package/dist/elements/EFMedia.d.ts +2 -3
  15. package/dist/elements/EFMedia.js +0 -4
  16. package/dist/elements/EFTemporal.d.ts +9 -6
  17. package/dist/elements/EFTemporal.js +15 -12
  18. package/dist/elements/EFTimegroup.browsertest.d.ts +26 -0
  19. package/dist/elements/EFTimegroup.d.ts +12 -9
  20. package/dist/elements/EFTimegroup.js +114 -65
  21. package/dist/elements/EFVideo.d.ts +5 -1
  22. package/dist/elements/EFVideo.js +16 -8
  23. package/dist/elements/EFWaveform.js +2 -3
  24. package/dist/elements/FetchContext.browsertest.d.ts +0 -0
  25. package/dist/elements/FetchMixin.js +14 -9
  26. package/dist/elements/TimegroupController.js +2 -1
  27. package/dist/elements/updateAnimations.browsertest.d.ts +0 -0
  28. package/dist/elements/updateAnimations.d.ts +19 -9
  29. package/dist/elements/updateAnimations.js +64 -25
  30. package/dist/gui/ContextMixin.js +34 -27
  31. package/dist/gui/EFConfiguration.d.ts +1 -1
  32. package/dist/gui/EFConfiguration.js +1 -0
  33. package/dist/gui/EFFilmstrip.d.ts +1 -0
  34. package/dist/gui/EFFilmstrip.js +12 -14
  35. package/dist/gui/TWMixin.js +1 -1
  36. package/dist/style.css +1 -1
  37. package/dist/transcoding/cache/URLTokenDeduplicator.d.ts +38 -0
  38. package/dist/transcoding/cache/URLTokenDeduplicator.js +66 -0
  39. package/dist/transcoding/cache/URLTokenDeduplicator.test.d.ts +1 -0
  40. package/dist/transcoding/types/index.d.ts +10 -0
  41. package/package.json +2 -2
  42. package/src/elements/EFMedia/AssetMediaEngine.ts +16 -2
  43. package/src/elements/EFMedia/BaseMediaEngine.ts +0 -6
  44. package/src/elements/EFMedia/JitMediaEngine.ts +14 -0
  45. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -1
  46. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +11 -4
  47. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -4
  48. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +4 -1
  49. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -5
  50. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +2 -2
  51. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +7 -3
  52. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +11 -4
  53. package/src/elements/EFMedia.browsertest.ts +13 -4
  54. package/src/elements/EFMedia.ts +6 -10
  55. package/src/elements/EFTemporal.ts +21 -26
  56. package/src/elements/EFTimegroup.browsertest.ts +186 -2
  57. package/src/elements/EFTimegroup.ts +190 -94
  58. package/src/elements/EFVideo.browsertest.ts +53 -132
  59. package/src/elements/EFVideo.ts +26 -13
  60. package/src/elements/EFWaveform.ts +2 -3
  61. package/src/elements/FetchContext.browsertest.ts +396 -0
  62. package/src/elements/FetchMixin.ts +25 -8
  63. package/src/elements/TimegroupController.ts +2 -1
  64. package/src/elements/updateAnimations.browsertest.ts +559 -0
  65. package/src/elements/updateAnimations.ts +113 -50
  66. package/src/gui/ContextMixin.browsertest.ts +4 -9
  67. package/src/gui/ContextMixin.ts +52 -33
  68. package/src/gui/EFConfiguration.ts +1 -1
  69. package/src/gui/EFFilmstrip.ts +15 -18
  70. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +182 -0
  71. package/src/transcoding/cache/URLTokenDeduplicator.ts +101 -0
  72. package/src/transcoding/types/index.ts +11 -0
  73. package/test/EFVideo.framegen.browsertest.ts +1 -1
  74. package/test/setup.ts +2 -0
  75. package/types.json +1 -1
@@ -1,10 +1,10 @@
1
1
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
2
2
  import { isContextMixin } from "../gui/ContextMixin.js";
3
3
  import { durationConverter } from "./durationConverter.js";
4
- import { EFTemporal, deepGetElementsWithFrameTasks, flushStartTimeMsCache, shallowGetTemporalElements, timegroupContext } from "./EFTemporal.js";
5
- import { updateAnimations } from "./updateAnimations.js";
4
+ import { EFTemporal, deepGetElementsWithFrameTasks, flushStartTimeMsCache, resetTemporalCache, shallowGetTemporalElements, timegroupContext } from "./EFTemporal.js";
6
5
  import { deepGetMediaElements } from "./EFMedia.js";
7
6
  import { TimegroupController } from "./TimegroupController.js";
7
+ import { evaluateTemporalState, updateAnimations } from "./updateAnimations.js";
8
8
  import { provide } from "@lit/context";
9
9
  import { Task, TaskStatus } from "@lit/task";
10
10
  import debug from "debug";
@@ -13,6 +13,10 @@ import { customElement, property } from "lit/decorators.js";
13
13
  import _decorate from "@oxc-project/runtime/helpers/decorate";
14
14
  var _EFTimegroup;
15
15
  const log = debug("ef:elements:EFTimegroup");
16
+ let sequenceDurationCache = /* @__PURE__ */ new WeakMap();
17
+ const flushSequenceDurationCache = () => {
18
+ sequenceDurationCache = /* @__PURE__ */ new WeakMap();
19
+ };
16
20
  const shallowGetTimegroups = (element, groups = []) => {
17
21
  for (const child of Array.from(element.children)) if (child instanceof EFTimegroup) groups.push(child);
18
22
  else shallowGetTimegroups(child, groups);
@@ -25,28 +29,32 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
25
29
  constructor(..._args) {
26
30
  super(..._args);
27
31
  this._timeGroupContext = this;
28
- this.mode = "contain";
29
- this.overlapMs = 0;
32
+ this._mode = "contain";
33
+ this._overlapMs = 0;
30
34
  this.fit = "none";
31
35
  this.frameTask = new Task(this, {
32
- autoRun: EF_INTERACTIVE,
36
+ autoRun: false,
33
37
  args: () => [this.ownCurrentTimeMs, this.currentTimeMs],
34
- task: async ([], { signal }) => {
35
- if (this.isRootTimegroup) await this.waitForFrameTasks(signal);
38
+ task: async ([]) => {
39
+ if (this.isRootTimegroup) {
40
+ await this.waitForFrameTasks();
41
+ updateAnimations(this);
42
+ }
36
43
  }
37
44
  });
38
45
  this.seekTask = new Task(this, {
46
+ autoRun: false,
39
47
  args: () => [this.#pendingSeekTime ?? this.#currentTime],
40
- task: async ([targetTime], { signal }) => {
41
- const newTime = Math.max(0, Math.min(targetTime, this.durationMs / 1e3));
42
- this.#currentTime = newTime;
48
+ onComplete: () => {},
49
+ task: async ([targetTime]) => {
50
+ if (!this.isRootTimegroup) return;
51
+ await this.waitForMediaDurations();
52
+ const newTime = Math.max(0, Math.min(targetTime ?? 0, this.durationMs / 1e3));
43
53
  this.requestUpdate("currentTime");
44
- await this.updateComplete;
45
- signal.throwIfAborted();
46
- const videoElements = this.querySelectorAll("ef-video");
47
- for (const video of videoElements) if (video.videoSeekTask) video.videoSeekTask.run();
48
54
  await this.frameTask.run();
49
55
  this.#saveTimeToLocalStorage(newTime);
56
+ this.#seekInProgress = false;
57
+ return newTime;
50
58
  }
51
59
  });
52
60
  }
@@ -62,72 +70,103 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
62
70
  }
63
71
  `;
64
72
  }
65
- #currentTime = 0;
73
+ #currentTime = void 0;
74
+ set mode(value) {
75
+ sequenceDurationCache.delete(this);
76
+ this._mode = value;
77
+ }
78
+ get mode() {
79
+ return this._mode;
80
+ }
81
+ set overlapMs(value) {
82
+ sequenceDurationCache.delete(this);
83
+ this._overlapMs = value;
84
+ }
85
+ get overlapMs() {
86
+ return this._overlapMs;
87
+ }
66
88
  #resizeObserver;
67
89
  #seekInProgress = false;
68
90
  #pendingSeekTime;
91
+ #processingPendingSeek = false;
69
92
  set currentTime(time) {
93
+ if (!this.isRootTimegroup) return;
94
+ if (Number.isNaN(time)) return;
95
+ if (time === this.#currentTime && !this.#processingPendingSeek) return;
96
+ if (this.#pendingSeekTime === time) return;
70
97
  if (this.#seekInProgress) {
98
+ console.trace("pending seek to", time);
71
99
  this.#pendingSeekTime = time;
100
+ this.#currentTime = time;
72
101
  return;
73
102
  }
103
+ console.trace("seeking to", time);
104
+ this.#currentTime = time;
74
105
  this.#seekInProgress = true;
75
- this.#pendingSeekTime = time;
76
106
  this.seekTask.run().finally(() => {
77
- this.#seekInProgress = false;
78
107
  if (this.#pendingSeekTime !== void 0 && this.#pendingSeekTime !== time) {
79
108
  const pendingTime = this.#pendingSeekTime;
80
109
  this.#pendingSeekTime = void 0;
81
- this.currentTime = pendingTime;
110
+ this.#processingPendingSeek = true;
111
+ try {
112
+ this.currentTime = pendingTime;
113
+ } finally {
114
+ this.#processingPendingSeek = false;
115
+ }
82
116
  } else this.#pendingSeekTime = void 0;
83
117
  });
84
118
  }
85
119
  get currentTime() {
86
- return this.#currentTime;
87
- }
88
- get currentTimeMs() {
89
- return this.currentTime * 1e3;
120
+ return this.#currentTime ?? 0;
90
121
  }
91
122
  set currentTimeMs(ms) {
92
123
  this.currentTime = ms / 1e3;
93
124
  }
125
+ get currentTimeMs() {
126
+ return this.currentTime * 1e3;
127
+ }
94
128
  /**
95
129
  * Determines if this is a root timegroup (no parent timegroups)
96
130
  */
97
131
  get isRootTimegroup() {
98
- return this.closest("ef-timegroup") === this;
132
+ return !this.parentTimegroup;
99
133
  }
100
134
  /**
101
135
  * Saves time to localStorage (extracted for reuse)
102
136
  */
103
137
  #saveTimeToLocalStorage(time) {
104
138
  try {
105
- if (this.id && this.isConnected) localStorage.setItem(this.storageKey, time.toString());
139
+ if (this.id && this.isConnected && !Number.isNaN(time)) localStorage.setItem(this.storageKey, time.toString());
106
140
  } catch (error) {
107
141
  log("Failed to save time to localStorage", error);
108
142
  }
109
143
  }
110
144
  render() {
111
- return html`<slot></slot> `;
145
+ return html`<slot @slotchange=${this.#handleSlotChange}></slot> `;
112
146
  }
147
+ #handleSlotChange = () => {
148
+ resetTemporalCache();
149
+ flushSequenceDurationCache();
150
+ flushStartTimeMsCache();
151
+ this.requestUpdate();
152
+ };
113
153
  maybeLoadTimeFromLocalStorage() {
114
154
  if (this.id) try {
115
- return Number.parseFloat(localStorage.getItem(this.storageKey) || "0");
155
+ const storedValue = localStorage.getItem(this.storageKey);
156
+ if (storedValue === null) return void 0;
157
+ return Number.parseFloat(storedValue);
116
158
  } catch (error) {
117
159
  log("Failed to load time from localStorage", error);
118
160
  }
119
- return 0;
120
161
  }
121
162
  connectedCallback() {
122
163
  super.connectedCallback();
123
164
  if (this.id) this.waitForMediaDurations().then(() => {
124
- this.currentTime = this.maybeLoadTimeFromLocalStorage();
165
+ const maybeLoadedTime = this.maybeLoadTimeFromLocalStorage();
166
+ if (maybeLoadedTime !== void 0) this.currentTime = maybeLoadedTime;
125
167
  });
126
168
  if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
127
169
  if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
128
- requestAnimationFrame(() => {
129
- this.updateAnimations();
130
- });
131
170
  }
132
171
  disconnectedCallback() {
133
172
  super.disconnectedCallback();
@@ -152,12 +191,15 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
152
191
  }
153
192
  case "fixed": return super.durationMs;
154
193
  case "sequence": {
194
+ const cachedDuration = sequenceDurationCache.get(this);
195
+ if (cachedDuration !== void 0) return cachedDuration;
155
196
  let duration = 0;
156
197
  this.childTemporals.forEach((child, index) => {
157
198
  if (child instanceof _EFTimegroup && child.mode === "fit") return;
158
199
  if (index > 0) duration -= this.overlapMs;
159
200
  duration += child.durationMs;
160
201
  });
202
+ sequenceDurationCache.set(this, duration);
161
203
  return duration;
162
204
  }
163
205
  case "contain": {
@@ -176,7 +218,21 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
176
218
  await this.waitForNestedUpdates(signal);
177
219
  signal?.throwIfAborted();
178
220
  const temporals = deepGetElementsWithFrameTasks(this);
179
- return temporals.map((temporal) => temporal.frameTask).filter((task) => task.status < TaskStatus.COMPLETE);
221
+ const timelineTimeMs = (this.#pendingSeekTime ?? this.#currentTime ?? 0) * 1e3;
222
+ const activeTemporals = temporals.filter((temporal) => {
223
+ if (!("startTimeMs" in temporal) || !("endTimeMs" in temporal)) return true;
224
+ const epsilon = .001;
225
+ const startTimeMs = temporal.startTimeMs;
226
+ const endTimeMs = temporal.endTimeMs;
227
+ const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
228
+ const elementEndsAfterStart = endTimeMs > timelineTimeMs;
229
+ return elementStartsBeforeEnd && elementEndsAfterStart;
230
+ });
231
+ const frameTasks = activeTemporals.map((temporal) => temporal.frameTask);
232
+ frameTasks.forEach((task) => {
233
+ task.run();
234
+ });
235
+ return frameTasks.filter((task) => task.status < TaskStatus.COMPLETE);
180
236
  }
181
237
  async waitForNestedUpdates(signal) {
182
238
  const limit = 10;
@@ -190,21 +246,15 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
190
246
  if (isComplete) break;
191
247
  }
192
248
  }
193
- async waitForFrameTasks(signal) {
194
- const limit = 10;
195
- let step = 0;
196
- await this.waitForNestedUpdates(signal);
197
- while (step < limit) {
198
- step++;
199
- let pendingTasks = await this.getPendingFrameTasks(signal);
200
- signal?.throwIfAborted();
201
- await Promise.all(pendingTasks.map((task) => task.taskComplete));
202
- signal?.throwIfAborted();
203
- await this.updateComplete;
204
- signal?.throwIfAborted();
205
- pendingTasks = await this.getPendingFrameTasks(signal);
206
- if (pendingTasks.length === 0) break;
207
- }
249
+ async waitForFrameTasks() {
250
+ const temporalElements = deepGetElementsWithFrameTasks(this);
251
+ const visibleElements = temporalElements.filter((element) => {
252
+ const temporalState = evaluateTemporalState(element);
253
+ return temporalState.isVisible;
254
+ });
255
+ await Promise.all(visibleElements.map((element) => {
256
+ return element.frameTask.run();
257
+ }));
208
258
  }
209
259
  /**
210
260
  * Wait for all media elements to load their initial segments.
@@ -215,21 +265,15 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
215
265
  async waitForMediaDurations() {
216
266
  await this.updateComplete;
217
267
  const mediaElements = deepGetMediaElements(this);
218
- await Promise.all(mediaElements.map((m) => m.mediaEngineTask.taskComplete));
268
+ await Promise.all(mediaElements.map((m) => m.mediaEngineTask.value ? Promise.resolve() : m.mediaEngineTask.run()));
219
269
  flushStartTimeMsCache();
270
+ flushSequenceDurationCache();
220
271
  this.requestUpdate("currentTime");
221
272
  await this.updateComplete;
222
273
  }
223
274
  get childTemporals() {
224
275
  return shallowGetTemporalElements(this);
225
276
  }
226
- updated(changedProperties) {
227
- super.updated(changedProperties);
228
- if (changedProperties.has("currentTime") || changedProperties.has("ownCurrentTimeMs")) this.updateAnimations();
229
- }
230
- updateAnimations() {
231
- updateAnimations(this);
232
- }
233
277
  get contextProvider() {
234
278
  let parent = this.parentNode;
235
279
  while (parent) {
@@ -276,19 +320,24 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
276
320
  const mediaLocalFromMs = Math.max(0, fromMs - mediaElement.startTimeMs);
277
321
  const mediaLocalToMs = Math.min(mediaElement.endTimeMs - mediaElement.startTimeMs, toMs - mediaElement.startTimeMs);
278
322
  if (mediaLocalFromMs >= mediaLocalToMs) return;
279
- const audio = await mediaElement.fetchAudioSpanningTime(mediaLocalFromMs, mediaLocalToMs, abortController.signal);
323
+ const sourceInMs = mediaElement.sourceInMs || mediaElement.trimStartMs || 0;
324
+ const mediaSourceFromMs = mediaLocalFromMs + sourceInMs;
325
+ const mediaSourceToMs = mediaLocalToMs + sourceInMs;
326
+ const audio = await mediaElement.fetchAudioSpanningTime(mediaSourceFromMs, mediaSourceToMs, abortController.signal);
280
327
  if (!audio) throw new Error("Failed to fetch audio");
281
328
  const bufferSource = audioContext.createBufferSource();
282
329
  bufferSource.buffer = await audioContext.decodeAudioData(await audio.blob.arrayBuffer());
283
330
  bufferSource.connect(audioContext.destination);
284
331
  const ctxStartMs = Math.max(0, mediaElement.startTimeMs - fromMs);
285
- const ctxEndMs = mediaElement.endTimeMs - fromMs;
286
- const ctxDurationMs = ctxEndMs - ctxStartMs;
287
- const requestedOffsetInMedia = mediaLocalFromMs;
288
- const actualOffsetInBuffer = requestedOffsetInMedia - audio.startMs;
289
- const safeOffset = Math.max(0, actualOffsetInBuffer);
290
- if (safeOffset !== actualOffsetInBuffer) {}
291
- bufferSource.start(ctxStartMs / 1e3, safeOffset / 1e3, ctxDurationMs / 1e3);
332
+ const requestedSourceFromMs = mediaSourceFromMs;
333
+ const actualSourceStartMs = audio.startMs;
334
+ const offsetInBufferMs = requestedSourceFromMs - actualSourceStartMs;
335
+ const safeOffsetMs = Math.max(0, offsetInBufferMs);
336
+ const requestedDurationMs = mediaSourceToMs - mediaSourceFromMs;
337
+ const availableAudioMs = audio.endMs - audio.startMs;
338
+ const actualDurationMs = Math.min(requestedDurationMs, availableAudioMs - safeOffsetMs);
339
+ if (actualDurationMs <= 0) return;
340
+ bufferSource.start(ctxStartMs / 1e3, safeOffsetMs / 1e3, actualDurationMs / 1e3);
292
341
  }));
293
342
  }
294
343
  async renderAudio(fromMs, toMs) {
@@ -347,12 +396,12 @@ _decorate([provide({ context: timegroupContext })], EFTimegroup.prototype, "_tim
347
396
  _decorate([property({
348
397
  type: String,
349
398
  attribute: "mode"
350
- })], EFTimegroup.prototype, "mode", void 0);
399
+ })], EFTimegroup.prototype, "mode", null);
351
400
  _decorate([property({
352
401
  type: Number,
353
402
  converter: durationConverter,
354
403
  attribute: "overlap"
355
- })], EFTimegroup.prototype, "overlapMs", void 0);
404
+ })], EFTimegroup.prototype, "overlapMs", null);
356
405
  _decorate([property({ type: String })], EFTimegroup.prototype, "fit", void 0);
357
406
  _decorate([property({
358
407
  type: Number,
@@ -49,10 +49,10 @@ export declare class EFVideo extends EFVideo_base {
49
49
  message: string;
50
50
  };
51
51
  constructor();
52
+ protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
52
53
  render(): import('lit-html').TemplateResult<1>;
53
54
  get canvasElement(): HTMLCanvasElement | undefined;
54
55
  frameTask: Task<readonly [number], void>;
55
- protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
56
56
  /**
57
57
  * Start a delayed loading operation for testing
58
58
  */
@@ -68,6 +68,10 @@ export declare class EFVideo extends EFVideo_base {
68
68
  */
69
69
  private setLoadingState;
70
70
  paintTask: Task<readonly [number], void>;
71
+ /**
72
+ * Clear the canvas when element becomes inactive
73
+ */
74
+ clearCanvas(): void;
71
75
  /**
72
76
  * Display a video frame on the canvas
73
77
  */
@@ -1,4 +1,3 @@
1
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
2
1
  import { EFMedia } from "./EFMedia.js";
3
2
  import { TWMixin } from "../gui/TWMixin2.js";
4
3
  import { DelayedLoadingState } from "../DelayedLoadingState.js";
@@ -99,19 +98,21 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
99
98
  message: ""
100
99
  };
101
100
  this.frameTask = new Task(this, {
102
- autoRun: EF_INTERACTIVE,
101
+ autoRun: false,
103
102
  args: () => [this.desiredSeekTimeMs],
104
103
  onError: (error) => {
105
104
  console.error("frameTask error", error);
106
105
  },
107
106
  onComplete: () => {},
108
- task: async ([_desiredSeekTimeMs], { signal }) => {
107
+ task: async ([_desiredSeekTimeMs]) => {
108
+ this.unifiedVideoSeekTask.run();
109
109
  await this.unifiedVideoSeekTask.taskComplete;
110
+ this.paintTask.run();
110
111
  await this.paintTask.taskComplete;
111
- if (signal.aborted) return;
112
112
  }
113
113
  });
114
114
  this.paintTask = new Task(this, {
115
+ autoRun: false,
115
116
  args: () => [this.desiredSeekTimeMs],
116
117
  onError: (error) => {
117
118
  console.error("paintTask error", error);
@@ -146,6 +147,9 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
146
147
  this.setLoadingState(isLoading, null, message);
147
148
  });
148
149
  }
150
+ updated(changedProperties) {
151
+ super.updated(changedProperties);
152
+ }
149
153
  render() {
150
154
  return html`
151
155
  <canvas ${ref(this.canvasRef)}></canvas>
@@ -169,9 +173,6 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
169
173
  if (shadowCanvas) return shadowCanvas;
170
174
  return void 0;
171
175
  }
172
- updated(changedProperties) {
173
- super.updated(changedProperties);
174
- }
175
176
  /**
176
177
  * Start a delayed loading operation for testing
177
178
  */
@@ -195,6 +196,14 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
195
196
  };
196
197
  }
197
198
  /**
199
+ * Clear the canvas when element becomes inactive
200
+ */
201
+ clearCanvas() {
202
+ if (!this.canvasElement) return;
203
+ const ctx = this.canvasElement.getContext("2d");
204
+ if (ctx) ctx.clearRect(0, 0, this.canvasElement.width, this.canvasElement.height);
205
+ }
206
+ /**
198
207
  * Display a video frame on the canvas
199
208
  */
200
209
  displayFrame(frame, seekToMs) {
@@ -225,7 +234,6 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
225
234
  log("trace: displayFrame aborted - null frame format");
226
235
  throw new Error(`Frame display failed: Video frame has null format at time ${seekToMs}ms. This indicates corrupted or incompatible video data.`);
227
236
  }
228
- log("trace: drawing frame to canvas", frame.timestamp / 1e3);
229
237
  ctx.drawImage(frame, 0, 0, this.canvasElement.width, this.canvasElement.height);
230
238
  log("trace: frame drawn to canvas", { seekToMs });
231
239
  return seekToMs;
@@ -1,4 +1,3 @@
1
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
2
1
  import { EF_RENDERING } from "../EF_RENDERING.js";
3
2
  import { EFTemporal } from "./EFTemporal.js";
4
3
  import { TargetController } from "./TargetController.js";
@@ -24,7 +23,7 @@ let EFWaveform = class EFWaveform$1 extends EFTemporal(TWMixin(LitElement)) {
24
23
  this.lineWidth = 4;
25
24
  this.targetController = new TargetController(this);
26
25
  this.frameTask = new Task(this, {
27
- autoRun: EF_INTERACTIVE,
26
+ autoRun: false,
28
27
  args: () => {
29
28
  return [this.targetElement, this.targetElement?.frequencyDataTask.value];
30
29
  },
@@ -95,7 +94,7 @@ let EFWaveform = class EFWaveform$1 extends EFTemporal(TWMixin(LitElement)) {
95
94
  `;
96
95
  }
97
96
  render() {
98
- return html`<canvas ${ref(this.canvasRef)}></canvas><div class="text-5xl inline-block bg-red-500">`;
97
+ return html`<canvas ${ref(this.canvasRef)}></canvas>`;
99
98
  }
100
99
  connectedCallback() {
101
100
  super.connectedCallback();
File without changes
@@ -1,18 +1,23 @@
1
- import { fetchContext } from "../gui/fetchContext.js";
2
- import { consume } from "@lit/context";
3
- import _decorate from "@oxc-project/runtime/helpers/decorate";
4
- import { state } from "lit/decorators/state.js";
5
1
  function FetchMixin(superClass) {
6
2
  class FetchElement extends superClass {
7
3
  constructor(..._args) {
8
4
  super(..._args);
9
- this.fetch = fetch.bind(window);
5
+ this.fetch = (url, init) => {
6
+ try {
7
+ const workbench = this.closest("ef-workbench");
8
+ if (workbench?.fetch) return workbench.fetch(url, init);
9
+ const preview = this.closest("ef-preview");
10
+ if (preview?.fetch) return preview.fetch(url, init);
11
+ const configuration = this.closest("ef-configuration");
12
+ if (configuration?.fetch) return configuration.fetch(url, init);
13
+ return window.fetch(url, init);
14
+ } catch (error) {
15
+ console.error("FetchMixin error", url, error);
16
+ throw error;
17
+ }
18
+ };
10
19
  }
11
20
  }
12
- _decorate([consume({
13
- context: fetchContext,
14
- subscribe: true
15
- }), state()], FetchElement.prototype, "fetch", void 0);
16
21
  return FetchElement;
17
22
  }
18
23
  export { FetchMixin };
@@ -12,7 +12,8 @@ var TimegroupController = class {
12
12
  }
13
13
  hostUpdated() {
14
14
  this.child.requestUpdate();
15
- this.child.currentTimeMs = this.host.currentTimeMs - (this.child.startTimeMs ?? 0);
15
+ const newChildTimeMs = this.host.currentTimeMs - (this.child.startTimeMs ?? 0);
16
+ this.child.currentTimeMs = newChildTimeMs;
16
17
  }
17
18
  };
18
19
  export { TimegroupController };
@@ -1,9 +1,19 @@
1
- import { EFTimegroup } from './EFTimegroup.ts';
2
- export declare const updateAnimations: (element: HTMLElement & {
3
- currentTimeMs: number;
4
- durationMs: number;
5
- rootTimegroup?: EFTimegroup;
6
- parentTimegroup?: EFTimegroup;
7
- startTimeMs: number;
8
- endTimeMs: number;
9
- }) => void;
1
+ import { TemporalMixinInterface } from './EFTemporal.ts';
2
+ export type AnimatableElement = TemporalMixinInterface & HTMLElement;
3
+ /**
4
+ * Represents the temporal state of an element relative to the timeline
5
+ */
6
+ interface TemporalState {
7
+ progress: number;
8
+ isVisible: boolean;
9
+ timelineTimeMs: number;
10
+ }
11
+ /**
12
+ * Evaluates what the element's state should be based on the timeline
13
+ */
14
+ export declare const evaluateTemporalState: (element: AnimatableElement) => TemporalState;
15
+ /**
16
+ * Main function: synchronizes DOM element with timeline
17
+ */
18
+ export declare const updateAnimations: (element: AnimatableElement) => void;
19
+ export {};
@@ -1,42 +1,81 @@
1
- import { isEFTemporal } from "./EFTemporal.js";
2
- const updateAnimations = (element) => {
3
- element.style.setProperty("--ef-progress", `${Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs)) * 100}%`);
1
+ import { deepGetTemporalElements, isEFTemporal } from "./EFTemporal.js";
2
+ const ANIMATION_PRECISION_OFFSET = Number.EPSILON;
3
+ const DEFAULT_ANIMATION_ITERATIONS = 1;
4
+ const PROGRESS_PROPERTY = "--ef-progress";
5
+ const DURATION_PROPERTY = "--ef-duration";
6
+ const TRANSITION_DURATION_PROPERTY = "--ef-transition-duration";
7
+ const TRANSITION_OUT_START_PROPERTY = "--ef-transition-out-start";
8
+ const TIMEGROUP_TAGNAME = "ef-timegroup";
9
+ /**
10
+ * Evaluates what the element's state should be based on the timeline
11
+ */
12
+ const evaluateTemporalState = (element) => {
4
13
  const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;
5
- if (element.startTimeMs > timelineTimeMs || element.endTimeMs < timelineTimeMs) {
6
- element.style.display = "none";
14
+ const progress = element.durationMs <= 0 ? 1 : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));
15
+ const isVisible = element.startTimeMs <= timelineTimeMs && element.endTimeMs > timelineTimeMs;
16
+ return {
17
+ progress,
18
+ isVisible,
19
+ timelineTimeMs
20
+ };
21
+ };
22
+ /**
23
+ * Updates the visual state (CSS + display) to match temporal state
24
+ */
25
+ const updateVisualState = (element, state) => {
26
+ element.style.setProperty(PROGRESS_PROPERTY, `${state.progress * 100}%`);
27
+ if (!state.isVisible) {
28
+ if (element.style.display !== "none") element.style.display = "none";
7
29
  return;
8
30
  }
9
- element.style.display = "";
10
- if (typeof element.getAnimations !== "function") return;
31
+ if (element.style.display === "none") element.style.display = "";
32
+ element.style.setProperty(DURATION_PROPERTY, `${element.durationMs}ms`);
33
+ element.style.setProperty(TRANSITION_DURATION_PROPERTY, `${element.parentTimegroup?.overlapMs ?? 0}ms`);
34
+ element.style.setProperty(TRANSITION_OUT_START_PROPERTY, `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`);
35
+ };
36
+ /**
37
+ * Coordinates animations to match timeline
38
+ */
39
+ const coordinateAnimations = (element) => {
11
40
  const animations = element.getAnimations({ subtree: true });
12
- element.style.setProperty("--ef-duration", `${element.durationMs}ms`);
13
- element.style.setProperty("--ef-transition-duration", `${element.parentTimegroup?.overlapMs ?? 0}ms`);
14
- element.style.setProperty("--ef-transition-out-start", `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`);
15
41
  for (const animation of animations) {
16
42
  if (animation.playState === "running") animation.pause();
17
43
  const effect = animation.effect;
18
44
  if (!(effect && effect instanceof KeyframeEffect)) continue;
19
45
  const target = effect.target;
20
- if (!target) continue;
21
- if (target.closest("ef-timegroup") !== element) continue;
22
- const timing = effect.getTiming();
23
- const duration = Number(timing.duration) ?? 0;
24
- const delay = Number(timing.delay) ?? 0;
25
- const iterations = Number(timing.iterations) ?? 1;
26
- const timeTarget = isEFTemporal(target) ? target : target.closest("ef-timegroup");
46
+ if (!target || target.closest(TIMEGROUP_TAGNAME) !== element) continue;
47
+ const timeTarget = isEFTemporal(target) ? target : target.closest(TIMEGROUP_TAGNAME);
27
48
  if (!timeTarget) continue;
28
- const currentTime = timeTarget.ownCurrentTimeMs;
29
- if (currentTime < delay) {
49
+ const timing = effect.getTiming();
50
+ const duration = Number(timing.duration) || 0;
51
+ const delay = Number(timing.delay) || 0;
52
+ const iterations = Number(timing.iterations) || DEFAULT_ANIMATION_ITERATIONS;
53
+ if (duration <= 0) {
30
54
  animation.currentTime = 0;
31
55
  continue;
32
56
  }
33
- const currentIteration = Math.floor((currentTime - delay) / duration);
34
- const currentIterationTime = (currentTime - delay) % duration;
35
- if (currentIteration >= iterations) {
36
- animation.currentTime = duration - .01;
57
+ const currentTime = timeTarget.ownCurrentTimeMs ?? 0;
58
+ if (currentTime < delay) {
59
+ animation.currentTime = 0;
37
60
  continue;
38
61
  }
39
- animation.currentTime = Math.min(currentIterationTime, duration - .01) + delay;
62
+ const adjustedTime = currentTime - delay;
63
+ const currentIteration = Math.floor(adjustedTime / duration);
64
+ const currentIterationTime = adjustedTime % duration;
65
+ if (currentIteration >= iterations) animation.currentTime = duration - ANIMATION_PRECISION_OFFSET;
66
+ else animation.currentTime = Math.min(currentIterationTime, duration - ANIMATION_PRECISION_OFFSET) + delay;
40
67
  }
41
68
  };
42
- export { updateAnimations };
69
+ /**
70
+ * Main function: synchronizes DOM element with timeline
71
+ */
72
+ const updateAnimations = (element) => {
73
+ const temporalState = evaluateTemporalState(element);
74
+ deepGetTemporalElements(element).forEach((temporalElement) => {
75
+ const temporalState$1 = evaluateTemporalState(temporalElement);
76
+ updateVisualState(temporalElement, temporalState$1);
77
+ });
78
+ updateVisualState(element, temporalState);
79
+ if (temporalState.isVisible) coordinateAnimations(element);
80
+ };
81
+ export { evaluateTemporalState, updateAnimations };