@editframe/elements 0.26.3-beta.0 → 0.26.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 (132) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-css.js +3 -3
  3. package/tsdown.config.ts +1 -1
  4. package/src/elements/ContextProxiesController.ts +0 -124
  5. package/src/elements/CrossUpdateController.ts +0 -22
  6. package/src/elements/EFAudio.browsertest.ts +0 -706
  7. package/src/elements/EFAudio.ts +0 -56
  8. package/src/elements/EFCaptions.browsertest.ts +0 -1960
  9. package/src/elements/EFCaptions.ts +0 -823
  10. package/src/elements/EFImage.browsertest.ts +0 -120
  11. package/src/elements/EFImage.ts +0 -113
  12. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
  13. package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
  14. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
  15. package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
  16. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
  17. package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
  18. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
  19. package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
  20. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
  21. package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
  22. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
  23. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
  24. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
  25. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
  26. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
  27. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
  28. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
  29. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
  30. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
  31. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
  32. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
  33. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
  34. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
  35. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
  36. package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
  37. package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
  38. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
  39. package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
  40. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
  41. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
  42. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
  43. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
  44. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
  45. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
  46. package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
  47. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
  48. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
  49. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
  50. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
  51. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
  52. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
  53. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
  54. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
  55. package/src/elements/EFMedia.browsertest.ts +0 -872
  56. package/src/elements/EFMedia.ts +0 -341
  57. package/src/elements/EFSourceMixin.ts +0 -60
  58. package/src/elements/EFSurface.browsertest.ts +0 -151
  59. package/src/elements/EFSurface.ts +0 -142
  60. package/src/elements/EFTemporal.browsertest.ts +0 -215
  61. package/src/elements/EFTemporal.ts +0 -800
  62. package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
  63. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
  64. package/src/elements/EFThumbnailStrip.ts +0 -906
  65. package/src/elements/EFTimegroup.browsertest.ts +0 -934
  66. package/src/elements/EFTimegroup.ts +0 -882
  67. package/src/elements/EFVideo.browsertest.ts +0 -1482
  68. package/src/elements/EFVideo.ts +0 -564
  69. package/src/elements/EFWaveform.ts +0 -547
  70. package/src/elements/FetchContext.browsertest.ts +0 -401
  71. package/src/elements/FetchMixin.ts +0 -38
  72. package/src/elements/SampleBuffer.ts +0 -94
  73. package/src/elements/TargetController.browsertest.ts +0 -230
  74. package/src/elements/TargetController.ts +0 -224
  75. package/src/elements/TimegroupController.ts +0 -26
  76. package/src/elements/durationConverter.ts +0 -35
  77. package/src/elements/parseTimeToMs.ts +0 -9
  78. package/src/elements/printTaskStatus.ts +0 -16
  79. package/src/elements/renderTemporalAudio.ts +0 -108
  80. package/src/elements/updateAnimations.browsertest.ts +0 -1884
  81. package/src/elements/updateAnimations.ts +0 -217
  82. package/src/elements/util.ts +0 -24
  83. package/src/gui/ContextMixin.browsertest.ts +0 -860
  84. package/src/gui/ContextMixin.ts +0 -562
  85. package/src/gui/Controllable.browsertest.ts +0 -258
  86. package/src/gui/Controllable.ts +0 -41
  87. package/src/gui/EFConfiguration.ts +0 -40
  88. package/src/gui/EFControls.browsertest.ts +0 -389
  89. package/src/gui/EFControls.ts +0 -195
  90. package/src/gui/EFDial.browsertest.ts +0 -84
  91. package/src/gui/EFDial.ts +0 -172
  92. package/src/gui/EFFilmstrip.browsertest.ts +0 -712
  93. package/src/gui/EFFilmstrip.ts +0 -1349
  94. package/src/gui/EFFitScale.ts +0 -152
  95. package/src/gui/EFFocusOverlay.ts +0 -79
  96. package/src/gui/EFPause.browsertest.ts +0 -202
  97. package/src/gui/EFPause.ts +0 -73
  98. package/src/gui/EFPlay.browsertest.ts +0 -202
  99. package/src/gui/EFPlay.ts +0 -73
  100. package/src/gui/EFPreview.ts +0 -74
  101. package/src/gui/EFResizableBox.browsertest.ts +0 -79
  102. package/src/gui/EFResizableBox.ts +0 -898
  103. package/src/gui/EFScrubber.ts +0 -151
  104. package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
  105. package/src/gui/EFTimeDisplay.ts +0 -55
  106. package/src/gui/EFToggleLoop.ts +0 -35
  107. package/src/gui/EFTogglePlay.ts +0 -70
  108. package/src/gui/EFWorkbench.ts +0 -115
  109. package/src/gui/PlaybackController.ts +0 -527
  110. package/src/gui/TWMixin.css +0 -6
  111. package/src/gui/TWMixin.ts +0 -61
  112. package/src/gui/TargetOrContextMixin.ts +0 -185
  113. package/src/gui/currentTimeContext.ts +0 -5
  114. package/src/gui/durationContext.ts +0 -3
  115. package/src/gui/efContext.ts +0 -6
  116. package/src/gui/fetchContext.ts +0 -5
  117. package/src/gui/focusContext.ts +0 -7
  118. package/src/gui/focusedElementContext.ts +0 -5
  119. package/src/gui/playingContext.ts +0 -5
  120. package/src/otel/BridgeSpanExporter.ts +0 -150
  121. package/src/otel/setupBrowserTracing.ts +0 -73
  122. package/src/otel/tracingHelpers.ts +0 -251
  123. package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
  124. package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
  125. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
  126. package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
  127. package/src/transcoding/types/index.ts +0 -312
  128. package/src/transcoding/utils/MediaUtils.ts +0 -63
  129. package/src/transcoding/utils/UrlGenerator.ts +0 -68
  130. package/src/transcoding/utils/constants.ts +0 -36
  131. package/src/utils/LRUCache.test.ts +0 -274
  132. package/src/utils/LRUCache.ts +0 -696
@@ -1,882 +0,0 @@
1
- import { provide } from "@lit/context";
2
- import { Task, TaskStatus } from "@lit/task";
3
- import debug from "debug";
4
- import { css, html, LitElement, type PropertyValues } from "lit";
5
- import { customElement, property } from "lit/decorators.js";
6
-
7
- import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
8
- import { EF_RENDERING } from "../EF_RENDERING.js";
9
- import { isContextMixin } from "../gui/ContextMixin.js";
10
- import { efContext } from "../gui/efContext.js";
11
- import { TWMixin } from "../gui/TWMixin.js";
12
- import { isTracingEnabled, withSpan } from "../otel/tracingHelpers.js";
13
- import { deepGetMediaElements, type EFMedia } from "./EFMedia.js";
14
- import {
15
- deepGetElementsWithFrameTasks,
16
- EFTemporal,
17
- flushStartTimeMsCache,
18
- resetTemporalCache,
19
- shallowGetTemporalElements,
20
- timegroupContext,
21
- } from "./EFTemporal.js";
22
- import { parseTimeToMs } from "./parseTimeToMs.js";
23
- import { renderTemporalAudio } from "./renderTemporalAudio.js";
24
- import { EFTargetable } from "./TargetController.js";
25
- import { TimegroupController } from "./TimegroupController.js";
26
- import {
27
- evaluateTemporalStateForAnimation,
28
- updateAnimations,
29
- } from "./updateAnimations.ts";
30
-
31
- declare global {
32
- var EF_DEV_WORKBENCH: boolean | undefined;
33
- }
34
-
35
- const log = debug("ef:elements:EFTimegroup");
36
-
37
- // Custom frame task callback type
38
- export type FrameTaskCallback = (info: {
39
- ownCurrentTimeMs: number;
40
- currentTimeMs: number;
41
- durationMs: number;
42
- percentComplete: number;
43
- element: EFTimegroup;
44
- }) => void | Promise<void>;
45
-
46
- // Cache for sequence mode duration calculations to avoid O(n) recalculation
47
- let sequenceDurationCache: WeakMap<EFTimegroup, number> = new WeakMap();
48
-
49
- export const flushSequenceDurationCache = () => {
50
- sequenceDurationCache = new WeakMap();
51
- };
52
-
53
- export const shallowGetTimegroups = (
54
- element: Element,
55
- groups: EFTimegroup[] = [],
56
- ) => {
57
- for (const child of Array.from(element.children)) {
58
- if (child instanceof EFTimegroup) {
59
- groups.push(child);
60
- } else {
61
- shallowGetTimegroups(child, groups);
62
- }
63
- }
64
- return groups;
65
- };
66
-
67
- @customElement("ef-timegroup")
68
- export class EFTimegroup extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
69
- static get observedAttributes(): string[] {
70
- // biome-ignore lint/complexity/noThisInStatic: It's okay to use this here
71
- const parentAttributes = super.observedAttributes || [];
72
- return [
73
- ...parentAttributes,
74
- "mode",
75
- "overlap",
76
- "currenttime",
77
- "fit",
78
- "fps",
79
- ];
80
- }
81
-
82
- static styles = css`
83
- :host {
84
- display: block;
85
- position: relative;
86
- overflow: hidden;
87
- }
88
-
89
- ::slotted(ef-timegroup) {
90
- position: absolute;
91
- width: 100%;
92
- height: 100%;
93
- top: 0;
94
- left: 0;
95
- overflow: initial;
96
- }
97
- `;
98
-
99
- @provide({ context: timegroupContext })
100
- _timeGroupContext = this;
101
-
102
- @provide({ context: efContext })
103
- efContext = this;
104
-
105
- mode: "fit" | "fixed" | "sequence" | "contain" = "contain";
106
- overlapMs = 0;
107
-
108
- @property({ type: Number })
109
- fps = 30;
110
-
111
- attributeChangedCallback(
112
- name: string,
113
- old: string | null,
114
- value: string | null,
115
- ): void {
116
- if (name === "mode" && value) {
117
- this.mode = value as typeof this.mode;
118
- }
119
- if (name === "overlap" && value) {
120
- this.overlapMs = parseTimeToMs(value);
121
- }
122
- if (name === "fps" && value) {
123
- this.fps = Number.parseFloat(value);
124
- }
125
- super.attributeChangedCallback(name, old, value);
126
- }
127
-
128
- @property({ type: String })
129
- fit: "none" | "contain" | "cover" = "none";
130
-
131
- #resizeObserver?: ResizeObserver;
132
-
133
- #currentTime: number | undefined = undefined;
134
- #seekInProgress = false;
135
- #pendingSeekTime: number | undefined;
136
- #processingPendingSeek = false;
137
- #customFrameTasks: Set<FrameTaskCallback> = new Set();
138
-
139
- /**
140
- * Get the effective FPS for this timegroup.
141
- * During rendering, uses the render options FPS if available.
142
- * Otherwise uses the configured fps property.
143
- */
144
- get effectiveFps(): number {
145
- // During rendering, prefer the render options FPS
146
- if (typeof window !== "undefined" && window.EF_FRAMEGEN?.renderOptions) {
147
- return window.EF_FRAMEGEN.renderOptions.encoderOptions.video.framerate;
148
- }
149
- return this.fps;
150
- }
151
-
152
- /**
153
- * Quantize a time value to the nearest frame boundary based on effectiveFps.
154
- * @param timeSeconds - Time in seconds
155
- * @returns Time quantized to frame boundaries in seconds
156
- */
157
- private quantizeToFrameTime(timeSeconds: number): number {
158
- const fps = this.effectiveFps;
159
- if (!fps || fps <= 0) return timeSeconds;
160
- const frameDurationS = 1 / fps;
161
- return Math.round(timeSeconds / frameDurationS) * frameDurationS;
162
- }
163
-
164
- private async runThrottledFrameTask(): Promise<void> {
165
- if (this.playbackController) {
166
- return this.playbackController.runThrottledFrameTask();
167
- }
168
- await this.frameTask.run();
169
- }
170
-
171
- @property({ type: Number, attribute: "currenttime" })
172
- set currentTime(time: number) {
173
- // Quantize time to frame boundaries based on fps
174
- // Do this BEFORE delegating to playbackController to ensure consistency
175
- time = this.quantizeToFrameTime(time);
176
-
177
- if (this.playbackController) {
178
- this.playbackController.currentTime = time;
179
- return;
180
- }
181
-
182
- time = Math.max(0, Math.min(this.durationMs / 1000, time));
183
- if (!this.isRootTimegroup) {
184
- return;
185
- }
186
- if (Number.isNaN(time)) {
187
- return;
188
- }
189
- if (time === this.#currentTime && !this.#processingPendingSeek) {
190
- return;
191
- }
192
- if (this.#pendingSeekTime === time) {
193
- return;
194
- }
195
-
196
- if (this.#seekInProgress) {
197
- this.#pendingSeekTime = time;
198
- this.#currentTime = time;
199
- return;
200
- }
201
-
202
- this.#currentTime = time;
203
- this.#seekInProgress = true;
204
-
205
- this.seekTask.run().finally(() => {
206
- if (
207
- this.#pendingSeekTime !== undefined &&
208
- this.#pendingSeekTime !== time
209
- ) {
210
- const pendingTime = this.#pendingSeekTime;
211
- this.#pendingSeekTime = undefined;
212
- this.#processingPendingSeek = true;
213
- try {
214
- this.currentTime = pendingTime;
215
- } finally {
216
- this.#processingPendingSeek = false;
217
- }
218
- } else {
219
- this.#pendingSeekTime = undefined;
220
- }
221
- });
222
- }
223
-
224
- get currentTime() {
225
- if (this.playbackController) {
226
- return this.playbackController.currentTime;
227
- }
228
- return this.#currentTime ?? 0;
229
- }
230
-
231
- set currentTimeMs(ms: number) {
232
- this.currentTime = ms / 1000;
233
- }
234
-
235
- get currentTimeMs() {
236
- return this.currentTime * 1000;
237
- }
238
-
239
- /**
240
- * Seek to a specific time and wait for all frames to be ready.
241
- * This is the recommended way to seek in tests and programmatic control.
242
- *
243
- * @param timeMs - Time in milliseconds to seek to
244
- * @returns Promise that resolves when the seek is complete and all visible children are ready
245
- */
246
- async seek(timeMs: number): Promise<void> {
247
- this.currentTimeMs = timeMs;
248
- await this.seekTask.taskComplete;
249
-
250
- // Handle localStorage when playbackController delegates seek
251
- if (this.playbackController) {
252
- this.saveTimeToLocalStorage(this.currentTime);
253
- }
254
-
255
- await this.frameTask.taskComplete;
256
-
257
- // Ensure all visible elements have completed their reactive update cycles AND frame rendering
258
- // waitForFrameTasks() calls frameTask.run() on children, but this may happen before child
259
- // elements have processed property changes from requestUpdate(). To ensure frame data is
260
- // accurate, we wait for updateComplete first, then ensure the frameTask has run with the
261
- // updated properties. Elements like EFVideo provide waitForFrameReady() for this pattern.
262
- const temporalElements = deepGetElementsWithFrameTasks(this);
263
- const visibleElements = temporalElements.filter((element) => {
264
- const animationState = evaluateTemporalStateForAnimation(element);
265
- return animationState.isVisible;
266
- });
267
-
268
- await Promise.all(
269
- visibleElements.map(async (element) => {
270
- if (
271
- "waitForFrameReady" in element &&
272
- typeof element.waitForFrameReady === "function"
273
- ) {
274
- await (element as any).waitForFrameReady();
275
- } else {
276
- await element.updateComplete;
277
- }
278
- }),
279
- );
280
- }
281
-
282
- /**
283
- * Determines if this is a root timegroup (no parent timegroups)
284
- */
285
- get isRootTimegroup(): boolean {
286
- return !this.parentTimegroup;
287
- }
288
-
289
- /**
290
- * Register a custom frame task callback that will be executed during frame rendering.
291
- * The callback receives timing information and can be async or sync.
292
- * Multiple callbacks can be registered and will execute in parallel.
293
- *
294
- * @param callback - Function to execute on each frame
295
- * @returns A cleanup function that removes the callback when called
296
- */
297
- addFrameTask(callback: FrameTaskCallback): () => void {
298
- if (typeof callback !== "function") {
299
- throw new Error("Frame task callback must be a function");
300
- }
301
- this.#customFrameTasks.add(callback);
302
- return () => {
303
- this.#customFrameTasks.delete(callback);
304
- };
305
- }
306
-
307
- /**
308
- * Remove a previously registered custom frame task callback.
309
- *
310
- * @param callback - The callback function to remove
311
- */
312
- removeFrameTask(callback: FrameTaskCallback): void {
313
- this.#customFrameTasks.delete(callback);
314
- }
315
-
316
- saveTimeToLocalStorage(time: number) {
317
- try {
318
- if (this.id && this.isConnected && !Number.isNaN(time)) {
319
- localStorage.setItem(this.storageKey, time.toString());
320
- }
321
- } catch (error) {
322
- log("Failed to save time to localStorage", error);
323
- }
324
- }
325
-
326
- render() {
327
- return html`<slot @slotchange=${this.#handleSlotChange}></slot> `;
328
- }
329
-
330
- #handleSlotChange = () => {
331
- // Invalidate caches when slot content changes
332
- resetTemporalCache();
333
- flushSequenceDurationCache();
334
- flushStartTimeMsCache();
335
-
336
- // Request update to trigger recalculation of dependent properties
337
- this.requestUpdate();
338
- };
339
-
340
- loadTimeFromLocalStorage(): number | undefined {
341
- if (this.id) {
342
- try {
343
- const storedValue = localStorage.getItem(this.storageKey);
344
- if (storedValue === null) {
345
- return undefined;
346
- }
347
- return Number.parseFloat(storedValue);
348
- } catch (error) {
349
- log("Failed to load time from localStorage", error);
350
- }
351
- }
352
- return undefined;
353
- }
354
-
355
- connectedCallback() {
356
- super.connectedCallback();
357
-
358
- if (!this.playbackController) {
359
- this.waitForMediaDurations().then(async () => {
360
- let didLoadFromStorage = false;
361
- if (this.id) {
362
- const maybeLoadedTime = this.loadTimeFromLocalStorage();
363
- if (maybeLoadedTime !== undefined) {
364
- this.currentTime = maybeLoadedTime;
365
- didLoadFromStorage = true;
366
- }
367
- }
368
- if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) {
369
- this.seekTask.run();
370
- } else if (didLoadFromStorage) {
371
- await this.seekTask.run();
372
- }
373
- });
374
- }
375
-
376
- if (this.parentTimegroup) {
377
- new TimegroupController(this.parentTimegroup, this);
378
- }
379
-
380
- if (this.shouldWrapWithWorkbench()) {
381
- this.wrapWithWorkbench();
382
- }
383
- }
384
-
385
- #previousDurationMs = 0;
386
-
387
- protected updated(changedProperties: PropertyValues): void {
388
- super.updated(changedProperties);
389
-
390
- if (changedProperties.has("mode") || changedProperties.has("overlapMs")) {
391
- sequenceDurationCache.delete(this);
392
- }
393
-
394
- if (this.#previousDurationMs !== this.durationMs) {
395
- this.#previousDurationMs = this.durationMs;
396
- this.runThrottledFrameTask();
397
- }
398
- }
399
-
400
- disconnectedCallback() {
401
- super.disconnectedCallback();
402
- this.#resizeObserver?.disconnect();
403
- }
404
-
405
- get storageKey() {
406
- if (!this.id) {
407
- throw new Error("Timegroup must have an id to use localStorage.");
408
- }
409
- return `ef-timegroup-${this.id}`;
410
- }
411
-
412
- get intrinsicDurationMs() {
413
- if (this.hasExplicitDuration) {
414
- return this.explicitDurationMs;
415
- }
416
- return undefined;
417
- }
418
-
419
- get hasOwnDuration() {
420
- return (
421
- this.mode === "contain" ||
422
- this.mode === "sequence" ||
423
- (this.mode === "fixed" && this.hasExplicitDuration)
424
- );
425
- }
426
-
427
- get durationMs(): number {
428
- switch (this.mode) {
429
- case "fit": {
430
- if (!this.parentTimegroup) {
431
- return 0;
432
- }
433
- return this.parentTimegroup.durationMs;
434
- }
435
- case "fixed":
436
- return super.durationMs;
437
- case "sequence": {
438
- // Check cache first to avoid expensive O(n) recalculation
439
- const cachedDuration = sequenceDurationCache.get(this);
440
- if (cachedDuration !== undefined) {
441
- return cachedDuration;
442
- }
443
-
444
- let duration = 0;
445
- this.childTemporals.forEach((child, index) => {
446
- if (child instanceof EFTimegroup && child.mode === "fit") {
447
- return;
448
- }
449
- if (index > 0) {
450
- duration -= this.overlapMs;
451
- }
452
- duration += child.durationMs;
453
- });
454
-
455
- // Cache the calculated duration
456
- sequenceDurationCache.set(this, duration);
457
- return duration;
458
- }
459
- case "contain": {
460
- let maxDuration = 0;
461
- for (const child of this.childTemporals) {
462
- // fit timegroups look "up" to their parent timegroup for their duration
463
- // so we need to skip them to avoid an infinite loop
464
- if (child instanceof EFTimegroup && child.mode === "fit") {
465
- continue;
466
- }
467
- if (!child.hasOwnDuration) {
468
- continue;
469
- }
470
- maxDuration = Math.max(maxDuration, child.durationMs);
471
- }
472
- return maxDuration;
473
- }
474
- default:
475
- throw new Error(`Invalid time mode: ${this.mode}`);
476
- }
477
- }
478
-
479
- async getPendingFrameTasks(signal?: AbortSignal) {
480
- await this.waitForNestedUpdates(signal);
481
- signal?.throwIfAborted();
482
- const temporals = deepGetElementsWithFrameTasks(this);
483
-
484
- // Filter to only include temporally visible elements for frame processing
485
- // (but keep all elements for duration calculations)
486
- // Use the target timeline time if we're in the middle of seeking
487
- const timelineTimeMs =
488
- (this.#pendingSeekTime ?? this.#currentTime ?? 0) * 1000;
489
- const activeTemporals = temporals.filter((temporal) => {
490
- // Skip timeline filtering if temporal doesn't have timeline position info
491
- if (!("startTimeMs" in temporal) || !("endTimeMs" in temporal)) {
492
- return true; // Keep non-temporal elements
493
- }
494
-
495
- // Only process frame tasks for elements that overlap the current timeline
496
- // Use same epsilon logic as seek task for consistency
497
- const epsilon = 0.001; // 1µs offset to break ties at boundaries
498
- const startTimeMs = (temporal as any).startTimeMs as number;
499
- const endTimeMs = (temporal as any).endTimeMs as number;
500
- const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
501
- // Root timegroups should remain visible at exact end time, but other elements use exclusive end for clean transitions
502
- const isRootTimegroup =
503
- temporal.tagName.toLowerCase() === "ef-timegroup" &&
504
- !(temporal as any).parentTimegroup;
505
- const useInclusiveEnd = isRootTimegroup;
506
- const elementEndsAfterStart = useInclusiveEnd
507
- ? endTimeMs >= timelineTimeMs
508
- : endTimeMs > timelineTimeMs;
509
- return elementStartsBeforeEnd && elementEndsAfterStart;
510
- });
511
-
512
- const frameTasks = activeTemporals.map((temporal) => temporal.frameTask);
513
- frameTasks.forEach((task) => {
514
- task.run();
515
- });
516
-
517
- return frameTasks.filter((task) => task.status < TaskStatus.COMPLETE);
518
- }
519
-
520
- async waitForNestedUpdates(signal?: AbortSignal) {
521
- const limit = 10;
522
- let steps = 0;
523
- let isComplete = true;
524
- while (true) {
525
- steps++;
526
- if (steps > limit) {
527
- throw new Error("Reached update depth limit.");
528
- }
529
- isComplete = await this.updateComplete;
530
- signal?.throwIfAborted();
531
- if (isComplete) {
532
- break;
533
- }
534
- }
535
- }
536
-
537
- async waitForFrameTasks() {
538
- const result = await withSpan(
539
- "timegroup.waitForFrameTasks",
540
- {
541
- timegroupId: this.id || "unknown",
542
- mode: this.mode,
543
- },
544
- undefined,
545
- async (span) => {
546
- const innerStart = performance.now();
547
-
548
- const temporalElements = deepGetElementsWithFrameTasks(this);
549
- if (isTracingEnabled()) {
550
- span.setAttribute("temporalElementsCount", temporalElements.length);
551
- }
552
-
553
- // Filter to only include temporally visible elements for frame processing
554
- // Use animation-friendly visibility to prevent animation jumps at exact boundaries
555
- const visibleElements = temporalElements.filter((element) => {
556
- const animationState = evaluateTemporalStateForAnimation(element);
557
- return animationState.isVisible;
558
- });
559
- if (isTracingEnabled()) {
560
- span.setAttribute("visibleElementsCount", visibleElements.length);
561
- }
562
-
563
- const promiseStart = performance.now();
564
-
565
- await Promise.all(
566
- visibleElements.map((element) => element.frameTask.run()),
567
- );
568
- const promiseEnd = performance.now();
569
-
570
- const innerEnd = performance.now();
571
- if (isTracingEnabled()) {
572
- span.setAttribute("actualInnerMs", innerEnd - innerStart);
573
- span.setAttribute("promiseAwaitMs", promiseEnd - promiseStart);
574
- }
575
- },
576
- );
577
-
578
- return result;
579
- }
580
-
581
- mediaDurationsPromise: Promise<void> | undefined = undefined;
582
-
583
- async waitForMediaDurations() {
584
- if (!this.mediaDurationsPromise) {
585
- this.mediaDurationsPromise = this.#waitForMediaDurations();
586
- }
587
- return this.mediaDurationsPromise;
588
- }
589
-
590
- /**
591
- * Wait for all media elements to load their initial segments.
592
- * Ideally we would only need the extracted index json data, but
593
- * that caused issues with constructing audio data. We had negative durations
594
- * in calculations and it was not clear why.
595
- */
596
- async #waitForMediaDurations() {
597
- return withSpan(
598
- "timegroup.waitForMediaDurations",
599
- {
600
- timegroupId: this.id || "unknown",
601
- mode: this.mode,
602
- },
603
- undefined,
604
- async (span) => {
605
- // We must await updateComplete to ensure all media elements inside this are connected
606
- // and will match deepGetMediaElements
607
- await this.updateComplete;
608
- const mediaElements = deepGetMediaElements(this);
609
- if (isTracingEnabled()) {
610
- span.setAttribute("mediaElementsCount", mediaElements.length);
611
- }
612
-
613
- // Then, we must await the fragmentIndexTask to ensure all media elements have their
614
- // fragment index loaded, which is where their duration is parsed from.
615
- await Promise.all(
616
- mediaElements.map((m) =>
617
- m.mediaEngineTask.value
618
- ? Promise.resolve()
619
- : m.mediaEngineTask.run(),
620
- ),
621
- );
622
-
623
- // After waiting for durations, we must force some updates to cascade and ensure all temporal elements
624
- // have correct durations and start times. It is not ideal that we have to do this inside here,
625
- // but it is the best current way to ensure that all temporal elements have correct durations and start times.
626
-
627
- // Next, we must flush the startTimeMs cache to ensure all media elements have their
628
- // startTimeMs parsed fresh, otherwise the startTimeMs is cached per animation frame.
629
- flushStartTimeMsCache();
630
-
631
- // Flush duration cache since child durations may have changed
632
- flushSequenceDurationCache();
633
-
634
- // Request an update to the currentTime of this group, ensuring that time updates will cascade
635
- // down to children, forcing sequence groups to arrange correctly.
636
- // This also makes the filmstrip update correctly.
637
- this.requestUpdate("currentTime");
638
- // Finally, we must await updateComplete to ensure all temporal elements have their
639
- // currentTime updated and all animations have run.
640
-
641
- await this.updateComplete;
642
- },
643
- );
644
- }
645
-
646
- get childTemporals() {
647
- return shallowGetTemporalElements(this);
648
- }
649
-
650
- get contextProvider() {
651
- let parent = this.parentNode;
652
- while (parent) {
653
- if (isContextMixin(parent)) {
654
- return parent;
655
- }
656
- parent = parent.parentNode;
657
- }
658
- return null;
659
- }
660
-
661
- /**
662
- * Returns true if the timegroup should be wrapped with a workbench.
663
- *
664
- * A timegroup should be wrapped with a workbench if:
665
- * - It's being rendered (EF_RENDERING), OR
666
- * - It's in interactive mode (EF_INTERACTIVE) with the dev workbench flag set
667
- *
668
- * If the timegroup is already wrapped in a context provider like ef-preview,
669
- * it should NOT be wrapped in a workbench.
670
- */
671
- shouldWrapWithWorkbench() {
672
- const isRendering = EF_RENDERING?.() === true;
673
-
674
- // During rendering, always wrap with workbench (needed by EF_FRAMEGEN)
675
- if (isRendering) {
676
- return (
677
- this.closest("ef-timegroup") === this &&
678
- this.closest("ef-preview") === null &&
679
- this.closest("ef-workbench") === null &&
680
- this.closest("test-context") === null
681
- );
682
- }
683
-
684
- // During interactive mode, respect the dev workbench flag
685
- if (!globalThis.EF_DEV_WORKBENCH) {
686
- return false;
687
- }
688
-
689
- return (
690
- EF_INTERACTIVE &&
691
- this.closest("ef-timegroup") === this &&
692
- this.closest("ef-preview") === null &&
693
- this.closest("ef-workbench") === null &&
694
- this.closest("test-context") === null
695
- );
696
- }
697
-
698
- wrapWithWorkbench() {
699
- const workbench = document.createElement("ef-workbench");
700
- this.parentElement?.append(workbench);
701
- if (!this.hasAttribute("id")) {
702
- this.setAttribute("id", "root-this");
703
- }
704
- this.setAttribute("slot", "canvas");
705
- workbench.append(this as unknown as Element);
706
-
707
- const filmstrip = document.createElement("ef-filmstrip");
708
- filmstrip.setAttribute("slot", "timeline");
709
- filmstrip.setAttribute("target", this.id);
710
- workbench.append(filmstrip);
711
- }
712
-
713
- get efElements() {
714
- return Array.from(
715
- this.querySelectorAll(
716
- "ef-audio, ef-video, ef-image, ef-captions, ef-waveform",
717
- ),
718
- );
719
- }
720
-
721
- /**
722
- * Returns media elements for playback audio rendering
723
- * For standalone media, returns [this]; for timegroups, returns all descendants
724
- * Used by PlaybackController for audio-driven playback
725
- */
726
- getMediaElements(): EFMedia[] {
727
- return deepGetMediaElements(this);
728
- }
729
-
730
- /**
731
- * Render audio buffer for playback
732
- * Called by PlaybackController during live playback
733
- * Delegates to shared renderTemporalAudio utility for consistent behavior
734
- */
735
- async renderAudio(fromMs: number, toMs: number): Promise<AudioBuffer> {
736
- return renderTemporalAudio(this, fromMs, toMs);
737
- }
738
-
739
- /**
740
- * TEMPORARY TEST METHOD: Renders audio and immediately plays it back
741
- * Usage: timegroup.testPlayAudio(0, 5000) // Play first 5 seconds
742
- */
743
- async testPlayAudio(fromMs: number, toMs: number) {
744
- // Render the audio using the existing renderAudio method
745
- const renderedBuffer = await this.renderAudio(fromMs, toMs);
746
-
747
- // Create a regular AudioContext for playback
748
- const playbackContext = new AudioContext();
749
-
750
- // Create a buffer source and connect it
751
- const bufferSource = playbackContext.createBufferSource();
752
- bufferSource.buffer = renderedBuffer;
753
- bufferSource.connect(playbackContext.destination);
754
-
755
- // Start playback immediately
756
- bufferSource.start(0);
757
-
758
- // Return a promise that resolves when playback ends
759
- return new Promise<void>((resolve) => {
760
- bufferSource.onended = () => {
761
- playbackContext.close();
762
- resolve();
763
- };
764
- });
765
- }
766
-
767
- async loadMd5Sums() {
768
- const efElements = this.efElements;
769
- const loaderTasks: Promise<any>[] = [];
770
- for (const el of efElements) {
771
- const md5SumLoader = (el as any).md5SumLoader;
772
- if (md5SumLoader instanceof Task) {
773
- md5SumLoader.run();
774
- loaderTasks.push(md5SumLoader.taskComplete);
775
- }
776
- }
777
-
778
- await Promise.all(loaderTasks);
779
-
780
- efElements.forEach((el) => {
781
- if ("productionSrc" in el && el.productionSrc instanceof Function) {
782
- el.setAttribute("src", el.productionSrc());
783
- }
784
- });
785
- }
786
-
787
- frameTask = new Task(this, {
788
- // autoRun: EF_INTERACTIVE,
789
- autoRun: false,
790
- args: () => [this.ownCurrentTimeMs, this.currentTimeMs] as const,
791
- task: async ([ownCurrentTimeMs, currentTimeMs]) => {
792
- if (this.isRootTimegroup) {
793
- await withSpan(
794
- "timegroup.frameTask",
795
- {
796
- timegroupId: this.id || "unknown",
797
- ownCurrentTimeMs,
798
- currentTimeMs,
799
- },
800
- undefined,
801
- async () => {
802
- await this.waitForFrameTasks();
803
- await this.#executeCustomFrameTasks();
804
- updateAnimations(this);
805
- },
806
- );
807
- } else {
808
- // Non-root timegroups execute their custom frame tasks when called
809
- await this.#executeCustomFrameTasks();
810
- }
811
- },
812
- });
813
-
814
- async #executeCustomFrameTasks() {
815
- if (this.#customFrameTasks.size > 0) {
816
- const percentComplete =
817
- this.durationMs > 0 ? this.ownCurrentTimeMs / this.durationMs : 0;
818
- const frameInfo = {
819
- ownCurrentTimeMs: this.ownCurrentTimeMs,
820
- currentTimeMs: this.currentTimeMs,
821
- durationMs: this.durationMs,
822
- percentComplete,
823
- element: this,
824
- };
825
-
826
- await Promise.all(
827
- Array.from(this.#customFrameTasks).map((callback) =>
828
- Promise.resolve(callback(frameInfo)),
829
- ),
830
- );
831
- }
832
- }
833
-
834
- seekTask = new Task(this, {
835
- autoRun: false,
836
- args: () => [this.#pendingSeekTime ?? this.#currentTime] as const,
837
- onComplete: () => {},
838
- task: async ([targetTime]) => {
839
- if (this.playbackController) {
840
- await this.playbackController.seekTask.taskComplete;
841
- return this.currentTime;
842
- }
843
-
844
- if (!this.isRootTimegroup) {
845
- return;
846
- }
847
- return withSpan(
848
- "timegroup.seekTask",
849
- {
850
- timegroupId: this.id || "unknown",
851
- targetTime: targetTime ?? 0,
852
- durationMs: this.durationMs,
853
- },
854
- undefined,
855
- async (span) => {
856
- await this.waitForMediaDurations();
857
- const newTime = Math.max(
858
- 0,
859
- Math.min(targetTime ?? 0, this.durationMs / 1000),
860
- );
861
- if (isTracingEnabled()) {
862
- span.setAttribute("newTime", newTime);
863
- }
864
- // Apply the clamped time back to currentTime
865
-
866
- this.#currentTime = newTime;
867
- this.requestUpdate("currentTime");
868
- await this.runThrottledFrameTask();
869
- this.saveTimeToLocalStorage(this.#currentTime);
870
- this.#seekInProgress = false;
871
- return newTime;
872
- },
873
- );
874
- },
875
- });
876
- }
877
-
878
- declare global {
879
- interface HTMLElementTagNameMap {
880
- "ef-timegroup": EFTimegroup & Element;
881
- }
882
- }