@editframe/elements 0.26.2-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 (135) hide show
  1. package/dist/elements/EFTimegroup.js +7 -2
  2. package/dist/elements/EFTimegroup.js.map +1 -1
  3. package/package.json +2 -2
  4. package/scripts/build-css.js +3 -3
  5. package/tsdown.config.ts +1 -1
  6. package/types.json +1 -1
  7. package/src/elements/ContextProxiesController.ts +0 -124
  8. package/src/elements/CrossUpdateController.ts +0 -22
  9. package/src/elements/EFAudio.browsertest.ts +0 -706
  10. package/src/elements/EFAudio.ts +0 -56
  11. package/src/elements/EFCaptions.browsertest.ts +0 -1960
  12. package/src/elements/EFCaptions.ts +0 -823
  13. package/src/elements/EFImage.browsertest.ts +0 -120
  14. package/src/elements/EFImage.ts +0 -113
  15. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
  16. package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
  17. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
  18. package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
  19. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
  20. package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
  21. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
  22. package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
  23. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
  24. package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
  25. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
  26. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
  27. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
  28. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
  29. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
  30. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
  31. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
  32. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
  33. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
  34. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
  35. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
  36. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
  37. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
  38. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
  39. package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
  40. package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
  41. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
  42. package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
  43. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
  44. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
  45. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
  46. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
  47. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
  48. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
  49. package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
  50. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
  51. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
  52. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
  53. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
  54. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
  55. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
  56. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
  57. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
  58. package/src/elements/EFMedia.browsertest.ts +0 -872
  59. package/src/elements/EFMedia.ts +0 -341
  60. package/src/elements/EFSourceMixin.ts +0 -60
  61. package/src/elements/EFSurface.browsertest.ts +0 -151
  62. package/src/elements/EFSurface.ts +0 -142
  63. package/src/elements/EFTemporal.browsertest.ts +0 -215
  64. package/src/elements/EFTemporal.ts +0 -800
  65. package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
  66. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
  67. package/src/elements/EFThumbnailStrip.ts +0 -906
  68. package/src/elements/EFTimegroup.browsertest.ts +0 -870
  69. package/src/elements/EFTimegroup.ts +0 -878
  70. package/src/elements/EFVideo.browsertest.ts +0 -1482
  71. package/src/elements/EFVideo.ts +0 -564
  72. package/src/elements/EFWaveform.ts +0 -547
  73. package/src/elements/FetchContext.browsertest.ts +0 -401
  74. package/src/elements/FetchMixin.ts +0 -38
  75. package/src/elements/SampleBuffer.ts +0 -94
  76. package/src/elements/TargetController.browsertest.ts +0 -230
  77. package/src/elements/TargetController.ts +0 -224
  78. package/src/elements/TimegroupController.ts +0 -26
  79. package/src/elements/durationConverter.ts +0 -35
  80. package/src/elements/parseTimeToMs.ts +0 -9
  81. package/src/elements/printTaskStatus.ts +0 -16
  82. package/src/elements/renderTemporalAudio.ts +0 -108
  83. package/src/elements/updateAnimations.browsertest.ts +0 -1884
  84. package/src/elements/updateAnimations.ts +0 -217
  85. package/src/elements/util.ts +0 -24
  86. package/src/gui/ContextMixin.browsertest.ts +0 -860
  87. package/src/gui/ContextMixin.ts +0 -562
  88. package/src/gui/Controllable.browsertest.ts +0 -258
  89. package/src/gui/Controllable.ts +0 -41
  90. package/src/gui/EFConfiguration.ts +0 -40
  91. package/src/gui/EFControls.browsertest.ts +0 -389
  92. package/src/gui/EFControls.ts +0 -195
  93. package/src/gui/EFDial.browsertest.ts +0 -84
  94. package/src/gui/EFDial.ts +0 -172
  95. package/src/gui/EFFilmstrip.browsertest.ts +0 -712
  96. package/src/gui/EFFilmstrip.ts +0 -1349
  97. package/src/gui/EFFitScale.ts +0 -152
  98. package/src/gui/EFFocusOverlay.ts +0 -79
  99. package/src/gui/EFPause.browsertest.ts +0 -202
  100. package/src/gui/EFPause.ts +0 -73
  101. package/src/gui/EFPlay.browsertest.ts +0 -202
  102. package/src/gui/EFPlay.ts +0 -73
  103. package/src/gui/EFPreview.ts +0 -74
  104. package/src/gui/EFResizableBox.browsertest.ts +0 -79
  105. package/src/gui/EFResizableBox.ts +0 -898
  106. package/src/gui/EFScrubber.ts +0 -151
  107. package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
  108. package/src/gui/EFTimeDisplay.ts +0 -55
  109. package/src/gui/EFToggleLoop.ts +0 -35
  110. package/src/gui/EFTogglePlay.ts +0 -70
  111. package/src/gui/EFWorkbench.ts +0 -115
  112. package/src/gui/PlaybackController.ts +0 -527
  113. package/src/gui/TWMixin.css +0 -6
  114. package/src/gui/TWMixin.ts +0 -61
  115. package/src/gui/TargetOrContextMixin.ts +0 -185
  116. package/src/gui/currentTimeContext.ts +0 -5
  117. package/src/gui/durationContext.ts +0 -3
  118. package/src/gui/efContext.ts +0 -6
  119. package/src/gui/fetchContext.ts +0 -5
  120. package/src/gui/focusContext.ts +0 -7
  121. package/src/gui/focusedElementContext.ts +0 -5
  122. package/src/gui/playingContext.ts +0 -5
  123. package/src/otel/BridgeSpanExporter.ts +0 -150
  124. package/src/otel/setupBrowserTracing.ts +0 -73
  125. package/src/otel/tracingHelpers.ts +0 -251
  126. package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
  127. package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
  128. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
  129. package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
  130. package/src/transcoding/types/index.ts +0 -312
  131. package/src/transcoding/utils/MediaUtils.ts +0 -63
  132. package/src/transcoding/utils/UrlGenerator.ts +0 -68
  133. package/src/transcoding/utils/constants.ts +0 -36
  134. package/src/utils/LRUCache.test.ts +0 -274
  135. package/src/utils/LRUCache.ts +0 -696
@@ -1,878 +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(() => {
360
- if (this.id) {
361
- const maybeLoadedTime = this.loadTimeFromLocalStorage();
362
- if (maybeLoadedTime !== undefined) {
363
- this.currentTime = maybeLoadedTime;
364
- }
365
- }
366
- if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) {
367
- this.seekTask.run();
368
- }
369
- });
370
- }
371
-
372
- if (this.parentTimegroup) {
373
- new TimegroupController(this.parentTimegroup, this);
374
- }
375
-
376
- if (this.shouldWrapWithWorkbench()) {
377
- this.wrapWithWorkbench();
378
- }
379
- }
380
-
381
- #previousDurationMs = 0;
382
-
383
- protected updated(changedProperties: PropertyValues): void {
384
- super.updated(changedProperties);
385
-
386
- if (changedProperties.has("mode") || changedProperties.has("overlapMs")) {
387
- sequenceDurationCache.delete(this);
388
- }
389
-
390
- if (this.#previousDurationMs !== this.durationMs) {
391
- this.#previousDurationMs = this.durationMs;
392
- this.runThrottledFrameTask();
393
- }
394
- }
395
-
396
- disconnectedCallback() {
397
- super.disconnectedCallback();
398
- this.#resizeObserver?.disconnect();
399
- }
400
-
401
- get storageKey() {
402
- if (!this.id) {
403
- throw new Error("Timegroup must have an id to use localStorage.");
404
- }
405
- return `ef-timegroup-${this.id}`;
406
- }
407
-
408
- get intrinsicDurationMs() {
409
- if (this.hasExplicitDuration) {
410
- return this.explicitDurationMs;
411
- }
412
- return undefined;
413
- }
414
-
415
- get hasOwnDuration() {
416
- return (
417
- this.mode === "contain" ||
418
- this.mode === "sequence" ||
419
- (this.mode === "fixed" && this.hasExplicitDuration)
420
- );
421
- }
422
-
423
- get durationMs(): number {
424
- switch (this.mode) {
425
- case "fit": {
426
- if (!this.parentTimegroup) {
427
- return 0;
428
- }
429
- return this.parentTimegroup.durationMs;
430
- }
431
- case "fixed":
432
- return super.durationMs;
433
- case "sequence": {
434
- // Check cache first to avoid expensive O(n) recalculation
435
- const cachedDuration = sequenceDurationCache.get(this);
436
- if (cachedDuration !== undefined) {
437
- return cachedDuration;
438
- }
439
-
440
- let duration = 0;
441
- this.childTemporals.forEach((child, index) => {
442
- if (child instanceof EFTimegroup && child.mode === "fit") {
443
- return;
444
- }
445
- if (index > 0) {
446
- duration -= this.overlapMs;
447
- }
448
- duration += child.durationMs;
449
- });
450
-
451
- // Cache the calculated duration
452
- sequenceDurationCache.set(this, duration);
453
- return duration;
454
- }
455
- case "contain": {
456
- let maxDuration = 0;
457
- for (const child of this.childTemporals) {
458
- // fit timegroups look "up" to their parent timegroup for their duration
459
- // so we need to skip them to avoid an infinite loop
460
- if (child instanceof EFTimegroup && child.mode === "fit") {
461
- continue;
462
- }
463
- if (!child.hasOwnDuration) {
464
- continue;
465
- }
466
- maxDuration = Math.max(maxDuration, child.durationMs);
467
- }
468
- return maxDuration;
469
- }
470
- default:
471
- throw new Error(`Invalid time mode: ${this.mode}`);
472
- }
473
- }
474
-
475
- async getPendingFrameTasks(signal?: AbortSignal) {
476
- await this.waitForNestedUpdates(signal);
477
- signal?.throwIfAborted();
478
- const temporals = deepGetElementsWithFrameTasks(this);
479
-
480
- // Filter to only include temporally visible elements for frame processing
481
- // (but keep all elements for duration calculations)
482
- // Use the target timeline time if we're in the middle of seeking
483
- const timelineTimeMs =
484
- (this.#pendingSeekTime ?? this.#currentTime ?? 0) * 1000;
485
- const activeTemporals = temporals.filter((temporal) => {
486
- // Skip timeline filtering if temporal doesn't have timeline position info
487
- if (!("startTimeMs" in temporal) || !("endTimeMs" in temporal)) {
488
- return true; // Keep non-temporal elements
489
- }
490
-
491
- // Only process frame tasks for elements that overlap the current timeline
492
- // Use same epsilon logic as seek task for consistency
493
- const epsilon = 0.001; // 1µs offset to break ties at boundaries
494
- const startTimeMs = (temporal as any).startTimeMs as number;
495
- const endTimeMs = (temporal as any).endTimeMs as number;
496
- const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
497
- // Root timegroups should remain visible at exact end time, but other elements use exclusive end for clean transitions
498
- const isRootTimegroup =
499
- temporal.tagName.toLowerCase() === "ef-timegroup" &&
500
- !(temporal as any).parentTimegroup;
501
- const useInclusiveEnd = isRootTimegroup;
502
- const elementEndsAfterStart = useInclusiveEnd
503
- ? endTimeMs >= timelineTimeMs
504
- : endTimeMs > timelineTimeMs;
505
- return elementStartsBeforeEnd && elementEndsAfterStart;
506
- });
507
-
508
- const frameTasks = activeTemporals.map((temporal) => temporal.frameTask);
509
- frameTasks.forEach((task) => {
510
- task.run();
511
- });
512
-
513
- return frameTasks.filter((task) => task.status < TaskStatus.COMPLETE);
514
- }
515
-
516
- async waitForNestedUpdates(signal?: AbortSignal) {
517
- const limit = 10;
518
- let steps = 0;
519
- let isComplete = true;
520
- while (true) {
521
- steps++;
522
- if (steps > limit) {
523
- throw new Error("Reached update depth limit.");
524
- }
525
- isComplete = await this.updateComplete;
526
- signal?.throwIfAborted();
527
- if (isComplete) {
528
- break;
529
- }
530
- }
531
- }
532
-
533
- async waitForFrameTasks() {
534
- const result = await withSpan(
535
- "timegroup.waitForFrameTasks",
536
- {
537
- timegroupId: this.id || "unknown",
538
- mode: this.mode,
539
- },
540
- undefined,
541
- async (span) => {
542
- const innerStart = performance.now();
543
-
544
- const temporalElements = deepGetElementsWithFrameTasks(this);
545
- if (isTracingEnabled()) {
546
- span.setAttribute("temporalElementsCount", temporalElements.length);
547
- }
548
-
549
- // Filter to only include temporally visible elements for frame processing
550
- // Use animation-friendly visibility to prevent animation jumps at exact boundaries
551
- const visibleElements = temporalElements.filter((element) => {
552
- const animationState = evaluateTemporalStateForAnimation(element);
553
- return animationState.isVisible;
554
- });
555
- if (isTracingEnabled()) {
556
- span.setAttribute("visibleElementsCount", visibleElements.length);
557
- }
558
-
559
- const promiseStart = performance.now();
560
-
561
- await Promise.all(
562
- visibleElements.map((element) => element.frameTask.run()),
563
- );
564
- const promiseEnd = performance.now();
565
-
566
- const innerEnd = performance.now();
567
- if (isTracingEnabled()) {
568
- span.setAttribute("actualInnerMs", innerEnd - innerStart);
569
- span.setAttribute("promiseAwaitMs", promiseEnd - promiseStart);
570
- }
571
- },
572
- );
573
-
574
- return result;
575
- }
576
-
577
- mediaDurationsPromise: Promise<void> | undefined = undefined;
578
-
579
- async waitForMediaDurations() {
580
- if (!this.mediaDurationsPromise) {
581
- this.mediaDurationsPromise = this.#waitForMediaDurations();
582
- }
583
- return this.mediaDurationsPromise;
584
- }
585
-
586
- /**
587
- * Wait for all media elements to load their initial segments.
588
- * Ideally we would only need the extracted index json data, but
589
- * that caused issues with constructing audio data. We had negative durations
590
- * in calculations and it was not clear why.
591
- */
592
- async #waitForMediaDurations() {
593
- return withSpan(
594
- "timegroup.waitForMediaDurations",
595
- {
596
- timegroupId: this.id || "unknown",
597
- mode: this.mode,
598
- },
599
- undefined,
600
- async (span) => {
601
- // We must await updateComplete to ensure all media elements inside this are connected
602
- // and will match deepGetMediaElements
603
- await this.updateComplete;
604
- const mediaElements = deepGetMediaElements(this);
605
- if (isTracingEnabled()) {
606
- span.setAttribute("mediaElementsCount", mediaElements.length);
607
- }
608
-
609
- // Then, we must await the fragmentIndexTask to ensure all media elements have their
610
- // fragment index loaded, which is where their duration is parsed from.
611
- await Promise.all(
612
- mediaElements.map((m) =>
613
- m.mediaEngineTask.value
614
- ? Promise.resolve()
615
- : m.mediaEngineTask.run(),
616
- ),
617
- );
618
-
619
- // After waiting for durations, we must force some updates to cascade and ensure all temporal elements
620
- // have correct durations and start times. It is not ideal that we have to do this inside here,
621
- // but it is the best current way to ensure that all temporal elements have correct durations and start times.
622
-
623
- // Next, we must flush the startTimeMs cache to ensure all media elements have their
624
- // startTimeMs parsed fresh, otherwise the startTimeMs is cached per animation frame.
625
- flushStartTimeMsCache();
626
-
627
- // Flush duration cache since child durations may have changed
628
- flushSequenceDurationCache();
629
-
630
- // Request an update to the currentTime of this group, ensuring that time updates will cascade
631
- // down to children, forcing sequence groups to arrange correctly.
632
- // This also makes the filmstrip update correctly.
633
- this.requestUpdate("currentTime");
634
- // Finally, we must await updateComplete to ensure all temporal elements have their
635
- // currentTime updated and all animations have run.
636
-
637
- await this.updateComplete;
638
- },
639
- );
640
- }
641
-
642
- get childTemporals() {
643
- return shallowGetTemporalElements(this);
644
- }
645
-
646
- get contextProvider() {
647
- let parent = this.parentNode;
648
- while (parent) {
649
- if (isContextMixin(parent)) {
650
- return parent;
651
- }
652
- parent = parent.parentNode;
653
- }
654
- return null;
655
- }
656
-
657
- /**
658
- * Returns true if the timegroup should be wrapped with a workbench.
659
- *
660
- * A timegroup should be wrapped with a workbench if:
661
- * - It's being rendered (EF_RENDERING), OR
662
- * - It's in interactive mode (EF_INTERACTIVE) with the dev workbench flag set
663
- *
664
- * If the timegroup is already wrapped in a context provider like ef-preview,
665
- * it should NOT be wrapped in a workbench.
666
- */
667
- shouldWrapWithWorkbench() {
668
- const isRendering = EF_RENDERING?.() === true;
669
-
670
- // During rendering, always wrap with workbench (needed by EF_FRAMEGEN)
671
- if (isRendering) {
672
- return (
673
- this.closest("ef-timegroup") === this &&
674
- this.closest("ef-preview") === null &&
675
- this.closest("ef-workbench") === null &&
676
- this.closest("test-context") === null
677
- );
678
- }
679
-
680
- // During interactive mode, respect the dev workbench flag
681
- if (!globalThis.EF_DEV_WORKBENCH) {
682
- return false;
683
- }
684
-
685
- return (
686
- EF_INTERACTIVE &&
687
- this.closest("ef-timegroup") === this &&
688
- this.closest("ef-preview") === null &&
689
- this.closest("ef-workbench") === null &&
690
- this.closest("test-context") === null
691
- );
692
- }
693
-
694
- wrapWithWorkbench() {
695
- const workbench = document.createElement("ef-workbench");
696
- this.parentElement?.append(workbench);
697
- if (!this.hasAttribute("id")) {
698
- this.setAttribute("id", "root-this");
699
- }
700
- this.setAttribute("slot", "canvas");
701
- workbench.append(this as unknown as Element);
702
-
703
- const filmstrip = document.createElement("ef-filmstrip");
704
- filmstrip.setAttribute("slot", "timeline");
705
- filmstrip.setAttribute("target", this.id);
706
- workbench.append(filmstrip);
707
- }
708
-
709
- get efElements() {
710
- return Array.from(
711
- this.querySelectorAll(
712
- "ef-audio, ef-video, ef-image, ef-captions, ef-waveform",
713
- ),
714
- );
715
- }
716
-
717
- /**
718
- * Returns media elements for playback audio rendering
719
- * For standalone media, returns [this]; for timegroups, returns all descendants
720
- * Used by PlaybackController for audio-driven playback
721
- */
722
- getMediaElements(): EFMedia[] {
723
- return deepGetMediaElements(this);
724
- }
725
-
726
- /**
727
- * Render audio buffer for playback
728
- * Called by PlaybackController during live playback
729
- * Delegates to shared renderTemporalAudio utility for consistent behavior
730
- */
731
- async renderAudio(fromMs: number, toMs: number): Promise<AudioBuffer> {
732
- return renderTemporalAudio(this, fromMs, toMs);
733
- }
734
-
735
- /**
736
- * TEMPORARY TEST METHOD: Renders audio and immediately plays it back
737
- * Usage: timegroup.testPlayAudio(0, 5000) // Play first 5 seconds
738
- */
739
- async testPlayAudio(fromMs: number, toMs: number) {
740
- // Render the audio using the existing renderAudio method
741
- const renderedBuffer = await this.renderAudio(fromMs, toMs);
742
-
743
- // Create a regular AudioContext for playback
744
- const playbackContext = new AudioContext();
745
-
746
- // Create a buffer source and connect it
747
- const bufferSource = playbackContext.createBufferSource();
748
- bufferSource.buffer = renderedBuffer;
749
- bufferSource.connect(playbackContext.destination);
750
-
751
- // Start playback immediately
752
- bufferSource.start(0);
753
-
754
- // Return a promise that resolves when playback ends
755
- return new Promise<void>((resolve) => {
756
- bufferSource.onended = () => {
757
- playbackContext.close();
758
- resolve();
759
- };
760
- });
761
- }
762
-
763
- async loadMd5Sums() {
764
- const efElements = this.efElements;
765
- const loaderTasks: Promise<any>[] = [];
766
- for (const el of efElements) {
767
- const md5SumLoader = (el as any).md5SumLoader;
768
- if (md5SumLoader instanceof Task) {
769
- md5SumLoader.run();
770
- loaderTasks.push(md5SumLoader.taskComplete);
771
- }
772
- }
773
-
774
- await Promise.all(loaderTasks);
775
-
776
- efElements.forEach((el) => {
777
- if ("productionSrc" in el && el.productionSrc instanceof Function) {
778
- el.setAttribute("src", el.productionSrc());
779
- }
780
- });
781
- }
782
-
783
- frameTask = new Task(this, {
784
- // autoRun: EF_INTERACTIVE,
785
- autoRun: false,
786
- args: () => [this.ownCurrentTimeMs, this.currentTimeMs] as const,
787
- task: async ([ownCurrentTimeMs, currentTimeMs]) => {
788
- if (this.isRootTimegroup) {
789
- await withSpan(
790
- "timegroup.frameTask",
791
- {
792
- timegroupId: this.id || "unknown",
793
- ownCurrentTimeMs,
794
- currentTimeMs,
795
- },
796
- undefined,
797
- async () => {
798
- await this.waitForFrameTasks();
799
- await this.#executeCustomFrameTasks();
800
- updateAnimations(this);
801
- },
802
- );
803
- } else {
804
- // Non-root timegroups execute their custom frame tasks when called
805
- await this.#executeCustomFrameTasks();
806
- }
807
- },
808
- });
809
-
810
- async #executeCustomFrameTasks() {
811
- if (this.#customFrameTasks.size > 0) {
812
- const percentComplete =
813
- this.durationMs > 0 ? this.ownCurrentTimeMs / this.durationMs : 0;
814
- const frameInfo = {
815
- ownCurrentTimeMs: this.ownCurrentTimeMs,
816
- currentTimeMs: this.currentTimeMs,
817
- durationMs: this.durationMs,
818
- percentComplete,
819
- element: this,
820
- };
821
-
822
- await Promise.all(
823
- Array.from(this.#customFrameTasks).map((callback) =>
824
- Promise.resolve(callback(frameInfo)),
825
- ),
826
- );
827
- }
828
- }
829
-
830
- seekTask = new Task(this, {
831
- autoRun: false,
832
- args: () => [this.#pendingSeekTime ?? this.#currentTime] as const,
833
- onComplete: () => {},
834
- task: async ([targetTime]) => {
835
- if (this.playbackController) {
836
- await this.playbackController.seekTask.taskComplete;
837
- return this.currentTime;
838
- }
839
-
840
- if (!this.isRootTimegroup) {
841
- return;
842
- }
843
- return withSpan(
844
- "timegroup.seekTask",
845
- {
846
- timegroupId: this.id || "unknown",
847
- targetTime: targetTime ?? 0,
848
- durationMs: this.durationMs,
849
- },
850
- undefined,
851
- async (span) => {
852
- await this.waitForMediaDurations();
853
- const newTime = Math.max(
854
- 0,
855
- Math.min(targetTime ?? 0, this.durationMs / 1000),
856
- );
857
- if (isTracingEnabled()) {
858
- span.setAttribute("newTime", newTime);
859
- }
860
- // Apply the clamped time back to currentTime
861
-
862
- this.#currentTime = newTime;
863
- this.requestUpdate("currentTime");
864
- await this.runThrottledFrameTask();
865
- this.saveTimeToLocalStorage(this.#currentTime);
866
- this.#seekInProgress = false;
867
- return newTime;
868
- },
869
- );
870
- },
871
- });
872
- }
873
-
874
- declare global {
875
- interface HTMLElementTagNameMap {
876
- "ef-timegroup": EFTimegroup & Element;
877
- }
878
- }