@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,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,33 @@ 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";
35
+ this.mediaDurationsPromise = void 0;
31
36
  this.frameTask = new Task(this, {
32
- autoRun: EF_INTERACTIVE,
37
+ autoRun: false,
33
38
  args: () => [this.ownCurrentTimeMs, this.currentTimeMs],
34
- task: async ([], { signal }) => {
35
- if (this.isRootTimegroup) await this.waitForFrameTasks(signal);
39
+ task: async ([]) => {
40
+ if (this.isRootTimegroup) {
41
+ await this.waitForFrameTasks();
42
+ updateAnimations(this);
43
+ }
36
44
  }
37
45
  });
38
46
  this.seekTask = new Task(this, {
47
+ autoRun: false,
39
48
  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;
49
+ onComplete: () => {},
50
+ task: async ([targetTime]) => {
51
+ if (!this.isRootTimegroup) return;
52
+ await this.waitForMediaDurations();
53
+ const newTime = Math.max(0, Math.min(targetTime ?? 0, this.durationMs / 1e3));
43
54
  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
55
  await this.frameTask.run();
49
56
  this.#saveTimeToLocalStorage(newTime);
57
+ this.#seekInProgress = false;
58
+ return newTime;
50
59
  }
51
60
  });
52
61
  }
@@ -62,72 +71,105 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
62
71
  }
63
72
  `;
64
73
  }
65
- #currentTime = 0;
74
+ #currentTime = void 0;
75
+ set mode(value) {
76
+ sequenceDurationCache.delete(this);
77
+ this._mode = value;
78
+ }
79
+ get mode() {
80
+ return this._mode;
81
+ }
82
+ set overlapMs(value) {
83
+ sequenceDurationCache.delete(this);
84
+ this._overlapMs = value;
85
+ }
86
+ get overlapMs() {
87
+ return this._overlapMs;
88
+ }
66
89
  #resizeObserver;
67
90
  #seekInProgress = false;
68
91
  #pendingSeekTime;
92
+ #processingPendingSeek = false;
69
93
  set currentTime(time) {
94
+ time = Math.max(0, time);
95
+ if (!this.isRootTimegroup) return;
96
+ if (Number.isNaN(time)) return;
97
+ if (time === this.#currentTime && !this.#processingPendingSeek) return;
98
+ if (this.#pendingSeekTime === time) return;
70
99
  if (this.#seekInProgress) {
71
100
  this.#pendingSeekTime = time;
101
+ this.#currentTime = time;
72
102
  return;
73
103
  }
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
- if (this.id) this.waitForMediaDurations().then(() => {
124
- this.currentTime = this.maybeLoadTimeFromLocalStorage();
164
+ this.waitForMediaDurations().then(() => {
165
+ if (this.id) {
166
+ const maybeLoadedTime = this.maybeLoadTimeFromLocalStorage();
167
+ if (maybeLoadedTime !== void 0) this.currentTime = maybeLoadedTime;
168
+ }
169
+ if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) this.seekTask.run();
125
170
  });
126
171
  if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
127
172
  if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
128
- requestAnimationFrame(() => {
129
- this.updateAnimations();
130
- });
131
173
  }
132
174
  disconnectedCallback() {
133
175
  super.disconnectedCallback();
@@ -152,12 +194,15 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
152
194
  }
153
195
  case "fixed": return super.durationMs;
154
196
  case "sequence": {
197
+ const cachedDuration = sequenceDurationCache.get(this);
198
+ if (cachedDuration !== void 0) return cachedDuration;
155
199
  let duration = 0;
156
200
  this.childTemporals.forEach((child, index) => {
157
201
  if (child instanceof _EFTimegroup && child.mode === "fit") return;
158
202
  if (index > 0) duration -= this.overlapMs;
159
203
  duration += child.durationMs;
160
204
  });
205
+ sequenceDurationCache.set(this, duration);
161
206
  return duration;
162
207
  }
163
208
  case "contain": {
@@ -176,7 +221,21 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
176
221
  await this.waitForNestedUpdates(signal);
177
222
  signal?.throwIfAborted();
178
223
  const temporals = deepGetElementsWithFrameTasks(this);
179
- return temporals.map((temporal) => temporal.frameTask).filter((task) => task.status < TaskStatus.COMPLETE);
224
+ const timelineTimeMs = (this.#pendingSeekTime ?? this.#currentTime ?? 0) * 1e3;
225
+ const activeTemporals = temporals.filter((temporal) => {
226
+ if (!("startTimeMs" in temporal) || !("endTimeMs" in temporal)) return true;
227
+ const epsilon = .001;
228
+ const startTimeMs = temporal.startTimeMs;
229
+ const endTimeMs = temporal.endTimeMs;
230
+ const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
231
+ const elementEndsAfterStart = endTimeMs > timelineTimeMs;
232
+ return elementStartsBeforeEnd && elementEndsAfterStart;
233
+ });
234
+ const frameTasks = activeTemporals.map((temporal) => temporal.frameTask);
235
+ frameTasks.forEach((task) => {
236
+ task.run();
237
+ });
238
+ return frameTasks.filter((task) => task.status < TaskStatus.COMPLETE);
180
239
  }
181
240
  async waitForNestedUpdates(signal) {
182
241
  const limit = 10;
@@ -190,21 +249,19 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
190
249
  if (isComplete) break;
191
250
  }
192
251
  }
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
- }
252
+ async waitForFrameTasks() {
253
+ const temporalElements = deepGetElementsWithFrameTasks(this);
254
+ const visibleElements = temporalElements.filter((element) => {
255
+ const temporalState = evaluateTemporalState(element);
256
+ return temporalState.isVisible;
257
+ });
258
+ await Promise.all(visibleElements.map((element) => {
259
+ return element.frameTask.run();
260
+ }));
261
+ }
262
+ async waitForMediaDurations() {
263
+ if (!this.mediaDurationsPromise) this.mediaDurationsPromise = this.#waitForMediaDurations();
264
+ return this.mediaDurationsPromise;
208
265
  }
209
266
  /**
210
267
  * Wait for all media elements to load their initial segments.
@@ -212,24 +269,18 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
212
269
  * that caused issues with constructing audio data. We had negative durations
213
270
  * in calculations and it was not clear why.
214
271
  */
215
- async waitForMediaDurations() {
272
+ async #waitForMediaDurations() {
216
273
  await this.updateComplete;
217
274
  const mediaElements = deepGetMediaElements(this);
218
- await Promise.all(mediaElements.map((m) => m.mediaEngineTask.taskComplete));
275
+ await Promise.all(mediaElements.map((m) => m.mediaEngineTask.value ? Promise.resolve() : m.mediaEngineTask.run()));
219
276
  flushStartTimeMsCache();
277
+ flushSequenceDurationCache();
220
278
  this.requestUpdate("currentTime");
221
279
  await this.updateComplete;
222
280
  }
223
281
  get childTemporals() {
224
282
  return shallowGetTemporalElements(this);
225
283
  }
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
284
  get contextProvider() {
234
285
  let parent = this.parentNode;
235
286
  while (parent) {
@@ -276,19 +327,24 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
276
327
  const mediaLocalFromMs = Math.max(0, fromMs - mediaElement.startTimeMs);
277
328
  const mediaLocalToMs = Math.min(mediaElement.endTimeMs - mediaElement.startTimeMs, toMs - mediaElement.startTimeMs);
278
329
  if (mediaLocalFromMs >= mediaLocalToMs) return;
279
- const audio = await mediaElement.fetchAudioSpanningTime(mediaLocalFromMs, mediaLocalToMs, abortController.signal);
330
+ const sourceInMs = mediaElement.sourceInMs || mediaElement.trimStartMs || 0;
331
+ const mediaSourceFromMs = mediaLocalFromMs + sourceInMs;
332
+ const mediaSourceToMs = mediaLocalToMs + sourceInMs;
333
+ const audio = await mediaElement.fetchAudioSpanningTime(mediaSourceFromMs, mediaSourceToMs, abortController.signal);
280
334
  if (!audio) throw new Error("Failed to fetch audio");
281
335
  const bufferSource = audioContext.createBufferSource();
282
336
  bufferSource.buffer = await audioContext.decodeAudioData(await audio.blob.arrayBuffer());
283
337
  bufferSource.connect(audioContext.destination);
284
338
  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);
339
+ const requestedSourceFromMs = mediaSourceFromMs;
340
+ const actualSourceStartMs = audio.startMs;
341
+ const offsetInBufferMs = requestedSourceFromMs - actualSourceStartMs;
342
+ const safeOffsetMs = Math.max(0, offsetInBufferMs);
343
+ const requestedDurationMs = mediaSourceToMs - mediaSourceFromMs;
344
+ const availableAudioMs = audio.endMs - audio.startMs;
345
+ const actualDurationMs = Math.min(requestedDurationMs, availableAudioMs - safeOffsetMs);
346
+ if (actualDurationMs <= 0) return;
347
+ bufferSource.start(ctxStartMs / 1e3, safeOffsetMs / 1e3, actualDurationMs / 1e3);
292
348
  }));
293
349
  }
294
350
  async renderAudio(fromMs, toMs) {
@@ -347,12 +403,12 @@ _decorate([provide({ context: timegroupContext })], EFTimegroup.prototype, "_tim
347
403
  _decorate([property({
348
404
  type: String,
349
405
  attribute: "mode"
350
- })], EFTimegroup.prototype, "mode", void 0);
406
+ })], EFTimegroup.prototype, "mode", null);
351
407
  _decorate([property({
352
408
  type: Number,
353
409
  converter: durationConverter,
354
410
  attribute: "overlap"
355
- })], EFTimegroup.prototype, "overlapMs", void 0);
411
+ })], EFTimegroup.prototype, "overlapMs", null);
356
412
  _decorate([property({ type: String })], EFTimegroup.prototype, "fit", void 0);
357
413
  _decorate([property({
358
414
  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 = .001;
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 };