@editframe/elements 0.21.0-beta.0 → 0.23.7-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 (142) hide show
  1. package/dist/EF_FRAMEGEN.js +2 -3
  2. package/dist/attachContextRoot.d.ts +1 -0
  3. package/dist/attachContextRoot.js +9 -0
  4. package/dist/elements/ContextProxiesController.d.ts +1 -2
  5. package/dist/elements/EFAudio.js +2 -2
  6. package/dist/elements/EFCaptions.d.ts +1 -3
  7. package/dist/elements/EFCaptions.js +59 -51
  8. package/dist/elements/EFImage.js +2 -2
  9. package/dist/elements/EFMedia/AssetIdMediaEngine.js +1 -2
  10. package/dist/elements/EFMedia/AssetMediaEngine.js +1 -3
  11. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +1 -1
  12. package/dist/elements/EFMedia/BufferedSeekingInput.js +2 -4
  13. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +4 -7
  14. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -2
  15. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +5 -9
  16. package/dist/elements/EFMedia/shared/BufferUtils.js +1 -3
  17. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +4 -7
  18. package/dist/elements/EFMedia.d.ts +19 -0
  19. package/dist/elements/EFMedia.js +19 -2
  20. package/dist/elements/EFSourceMixin.js +1 -1
  21. package/dist/elements/EFSurface.js +1 -1
  22. package/dist/elements/EFTemporal.browsertest.d.ts +11 -0
  23. package/dist/elements/EFTemporal.d.ts +10 -0
  24. package/dist/elements/EFTemporal.js +82 -5
  25. package/dist/elements/EFThumbnailStrip.js +9 -16
  26. package/dist/elements/EFTimegroup.browsertest.d.ts +3 -3
  27. package/dist/elements/EFTimegroup.d.ts +35 -14
  28. package/dist/elements/EFTimegroup.js +73 -120
  29. package/dist/elements/EFVideo.d.ts +10 -0
  30. package/dist/elements/EFVideo.js +15 -2
  31. package/dist/elements/EFWaveform.js +10 -18
  32. package/dist/elements/SampleBuffer.js +1 -2
  33. package/dist/elements/TargetController.js +2 -2
  34. package/dist/elements/renderTemporalAudio.d.ts +10 -0
  35. package/dist/elements/renderTemporalAudio.js +35 -0
  36. package/dist/elements/updateAnimations.js +7 -10
  37. package/dist/gui/ContextMixin.d.ts +5 -5
  38. package/dist/gui/ContextMixin.js +151 -117
  39. package/dist/gui/Controllable.browsertest.d.ts +0 -0
  40. package/dist/gui/Controllable.d.ts +15 -0
  41. package/dist/gui/Controllable.js +9 -0
  42. package/dist/gui/EFConfiguration.js +1 -1
  43. package/dist/gui/EFControls.browsertest.d.ts +11 -0
  44. package/dist/gui/EFControls.d.ts +18 -4
  45. package/dist/gui/EFControls.js +67 -25
  46. package/dist/gui/EFDial.browsertest.d.ts +0 -0
  47. package/dist/gui/EFDial.d.ts +18 -0
  48. package/dist/gui/EFDial.js +141 -0
  49. package/dist/gui/EFFilmstrip.browsertest.d.ts +11 -0
  50. package/dist/gui/EFFilmstrip.d.ts +12 -2
  51. package/dist/gui/EFFilmstrip.js +140 -34
  52. package/dist/gui/EFFitScale.js +2 -4
  53. package/dist/gui/EFFocusOverlay.js +1 -1
  54. package/dist/gui/EFPause.browsertest.d.ts +0 -0
  55. package/dist/gui/EFPause.d.ts +23 -0
  56. package/dist/gui/EFPause.js +59 -0
  57. package/dist/gui/EFPlay.browsertest.d.ts +0 -0
  58. package/dist/gui/EFPlay.d.ts +23 -0
  59. package/dist/gui/EFPlay.js +59 -0
  60. package/dist/gui/EFPreview.d.ts +4 -0
  61. package/dist/gui/EFPreview.js +15 -6
  62. package/dist/gui/EFResizableBox.browsertest.d.ts +0 -0
  63. package/dist/gui/EFResizableBox.d.ts +34 -0
  64. package/dist/gui/EFResizableBox.js +547 -0
  65. package/dist/gui/EFScrubber.d.ts +9 -3
  66. package/dist/gui/EFScrubber.js +7 -7
  67. package/dist/gui/EFTimeDisplay.d.ts +7 -1
  68. package/dist/gui/EFTimeDisplay.js +5 -5
  69. package/dist/gui/EFToggleLoop.d.ts +9 -3
  70. package/dist/gui/EFToggleLoop.js +6 -4
  71. package/dist/gui/EFTogglePlay.d.ts +12 -4
  72. package/dist/gui/EFTogglePlay.js +24 -19
  73. package/dist/gui/EFWorkbench.js +1 -1
  74. package/dist/gui/PlaybackController.d.ts +67 -0
  75. package/dist/gui/PlaybackController.js +310 -0
  76. package/dist/gui/TWMixin.js +1 -1
  77. package/dist/gui/TargetOrContextMixin.d.ts +10 -0
  78. package/dist/gui/TargetOrContextMixin.js +98 -0
  79. package/dist/gui/efContext.d.ts +2 -2
  80. package/dist/index.d.ts +4 -0
  81. package/dist/index.js +5 -1
  82. package/dist/otel/setupBrowserTracing.d.ts +1 -1
  83. package/dist/otel/setupBrowserTracing.js +6 -4
  84. package/dist/otel/tracingHelpers.js +1 -2
  85. package/dist/style.css +1 -1
  86. package/package.json +5 -5
  87. package/src/elements/ContextProxiesController.ts +10 -10
  88. package/src/elements/EFAudio.ts +1 -0
  89. package/src/elements/EFCaptions.browsertest.ts +128 -58
  90. package/src/elements/EFCaptions.ts +60 -34
  91. package/src/elements/EFImage.browsertest.ts +1 -2
  92. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +3 -0
  93. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +1 -1
  94. package/src/elements/EFMedia.browsertest.ts +8 -15
  95. package/src/elements/EFMedia.ts +38 -7
  96. package/src/elements/EFSurface.browsertest.ts +2 -6
  97. package/src/elements/EFSurface.ts +1 -0
  98. package/src/elements/EFTemporal.browsertest.ts +58 -1
  99. package/src/elements/EFTemporal.ts +140 -4
  100. package/src/elements/EFThumbnailStrip.browsertest.ts +2 -8
  101. package/src/elements/EFThumbnailStrip.ts +1 -0
  102. package/src/elements/EFTimegroup.browsertest.ts +6 -7
  103. package/src/elements/EFTimegroup.ts +163 -244
  104. package/src/elements/EFVideo.browsertest.ts +143 -47
  105. package/src/elements/EFVideo.ts +26 -0
  106. package/src/elements/FetchContext.browsertest.ts +7 -2
  107. package/src/elements/TargetController.browsertest.ts +1 -0
  108. package/src/elements/TargetController.ts +1 -0
  109. package/src/elements/renderTemporalAudio.ts +108 -0
  110. package/src/elements/updateAnimations.browsertest.ts +181 -6
  111. package/src/elements/updateAnimations.ts +6 -6
  112. package/src/gui/ContextMixin.browsertest.ts +274 -27
  113. package/src/gui/ContextMixin.ts +230 -175
  114. package/src/gui/Controllable.browsertest.ts +258 -0
  115. package/src/gui/Controllable.ts +41 -0
  116. package/src/gui/EFControls.browsertest.ts +294 -80
  117. package/src/gui/EFControls.ts +139 -28
  118. package/src/gui/EFDial.browsertest.ts +84 -0
  119. package/src/gui/EFDial.ts +172 -0
  120. package/src/gui/EFFilmstrip.browsertest.ts +712 -0
  121. package/src/gui/EFFilmstrip.ts +213 -23
  122. package/src/gui/EFPause.browsertest.ts +202 -0
  123. package/src/gui/EFPause.ts +73 -0
  124. package/src/gui/EFPlay.browsertest.ts +202 -0
  125. package/src/gui/EFPlay.ts +73 -0
  126. package/src/gui/EFPreview.ts +20 -5
  127. package/src/gui/EFResizableBox.browsertest.ts +79 -0
  128. package/src/gui/EFResizableBox.ts +898 -0
  129. package/src/gui/EFScrubber.ts +7 -5
  130. package/src/gui/EFTimeDisplay.browsertest.ts +19 -19
  131. package/src/gui/EFTimeDisplay.ts +3 -1
  132. package/src/gui/EFToggleLoop.ts +6 -5
  133. package/src/gui/EFTogglePlay.ts +30 -23
  134. package/src/gui/PlaybackController.ts +522 -0
  135. package/src/gui/TWMixin.css +3 -0
  136. package/src/gui/TargetOrContextMixin.ts +185 -0
  137. package/src/gui/efContext.ts +2 -2
  138. package/src/otel/setupBrowserTracing.ts +17 -12
  139. package/test/cache-integration-verification.browsertest.ts +1 -1
  140. package/types.json +1 -1
  141. package/dist/elements/ContextProxiesController.js +0 -49
  142. /package/dist/_virtual/{_@oxc-project_runtime@0.93.0 → _@oxc-project_runtime@0.94.0}/helpers/decorate.js +0 -0
@@ -1,8 +1,11 @@
1
- import { consume, createContext, provide } from "@lit/context";
1
+ import { ContextProvider, consume, createContext, provide } from "@lit/context";
2
2
  import type { LitElement } from "lit";
3
3
  import { property, state } from "lit/decorators.js";
4
4
  import { EF_RENDERING } from "../EF_RENDERING.ts";
5
- import type { EFTimegroup } from "../elements/EFTimegroup.js";
5
+ import {
6
+ isEFTemporal,
7
+ type TemporalMixinInterface,
8
+ } from "../elements/EFTemporal.js";
6
9
  import { globalURLTokenDeduplicator } from "../transcoding/cache/URLTokenDeduplicator.js";
7
10
  import { currentTimeContext } from "./currentTimeContext.js";
8
11
  import { durationContext } from "./durationContext.js";
@@ -16,9 +19,8 @@ import { type FocusContext, focusContext } from "./focusContext.js";
16
19
  import { focusedElementContext } from "./focusedElementContext.js";
17
20
  import { loopContext, playingContext } from "./playingContext.js";
18
21
 
19
- export const targetTimegroupContext = createContext<EFTimegroup | null>(
20
- "target-timegroup",
21
- );
22
+ export const targetTemporalContext =
23
+ createContext<TemporalMixinInterface | null>(Symbol("target-temporal"));
22
24
 
23
25
  export declare class ContextMixinInterface extends LitElement {
24
26
  signingURL?: string;
@@ -28,8 +30,8 @@ export declare class ContextMixinInterface extends LitElement {
28
30
  loop: boolean;
29
31
  currentTimeMs: number;
30
32
  focusedElement?: HTMLElement;
31
- targetTimegroup: EFTimegroup | null;
32
- play(): void;
33
+ targetTemporal: TemporalMixinInterface | null;
34
+ play(): Promise<void>;
33
35
  pause(): void;
34
36
  }
35
37
 
@@ -58,6 +60,13 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
58
60
  @state()
59
61
  focusedElement?: HTMLElement;
60
62
 
63
+ #playingProvider!: ContextProvider<typeof playingContext>;
64
+ #loopProvider!: ContextProvider<typeof loopContext>;
65
+ #currentTimeMsProvider!: ContextProvider<typeof currentTimeContext>;
66
+ #targetTemporalProvider!: ContextProvider<typeof targetTemporalContext>;
67
+
68
+ #loop = false;
69
+
61
70
  #apiHost?: string;
62
71
  @property({ type: String, attribute: "api-host" })
63
72
  get apiHost() {
@@ -71,11 +80,97 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
71
80
  @provide({ context: efContext })
72
81
  efContext = this;
73
82
 
74
- @provide({ context: targetTimegroupContext })
83
+ #targetTemporal: TemporalMixinInterface | null = null;
84
+
75
85
  @state()
76
- targetTimegroup: EFTimegroup | null = null;
86
+ get targetTemporal(): TemporalMixinInterface | null {
87
+ return this.#targetTemporal;
88
+ }
89
+ #controllerSubscribed = false;
90
+
91
+ /**
92
+ * Find the first root temporal element (recursively searches through children)
93
+ * Supports ef-timegroup, ef-video, ef-audio, and any other temporal elements
94
+ * even when they're wrapped in non-temporal elements like divs
95
+ */
96
+ private findRootTemporal(): TemporalMixinInterface | null {
97
+ const findRecursive = (
98
+ element: Element,
99
+ ): TemporalMixinInterface | null => {
100
+ if (isEFTemporal(element)) {
101
+ return element as TemporalMixinInterface & HTMLElement;
102
+ }
103
+
104
+ for (const child of element.children) {
105
+ const found = findRecursive(child);
106
+ if (found) return found;
107
+ }
108
+
109
+ return null;
110
+ };
111
+
112
+ for (const child of this.children) {
113
+ const found = findRecursive(child);
114
+ if (found) return found;
115
+ }
116
+
117
+ return null;
118
+ }
119
+
120
+ set targetTemporal(value: TemporalMixinInterface | null) {
121
+ if (this.#targetTemporal === value) return;
122
+
123
+ // Unsubscribe from old controller updates
124
+ if (this.#targetTemporal?.playbackController) {
125
+ this.#targetTemporal.playbackController.removeListener(
126
+ this.#onControllerUpdate,
127
+ );
128
+ this.#controllerSubscribed = false;
129
+ }
130
+
131
+ this.#targetTemporal = value;
132
+ this.#targetTemporalProvider?.setValue(value);
133
+
134
+ // Sync all provided contexts
135
+ this.requestUpdate("targetTemporal");
136
+ this.requestUpdate("playing");
137
+ this.requestUpdate("loop");
138
+ this.requestUpdate("currentTimeMs");
139
+
140
+ // If the new targetTemporal has a playbackController, apply stored loop value immediately
141
+ if (value?.playbackController && this.#loop) {
142
+ value.playbackController.setLoop(this.#loop);
143
+ }
77
144
 
78
- // Add reactive properties that depend on the targetTimegroup
145
+ // If the new targetTemporal doesn't have a playbackController yet,
146
+ // wait for it to complete its updates (it might be initializing)
147
+ if (value && !value.playbackController) {
148
+ // Wait for the temporal element to initialize
149
+ (value as any).updateComplete?.then(() => {
150
+ if (value === this.#targetTemporal && !this.#controllerSubscribed) {
151
+ this.requestUpdate();
152
+ }
153
+ });
154
+ }
155
+ }
156
+
157
+ #onControllerUpdate = (
158
+ event: import("./PlaybackController.js").PlaybackControllerUpdateEvent,
159
+ ) => {
160
+ switch (event.property) {
161
+ case "playing":
162
+ this.#playingProvider.setValue(event.value as boolean);
163
+ break;
164
+ case "loop":
165
+ this.#loopProvider.setValue(event.value as boolean);
166
+ break;
167
+ case "currentTimeMs":
168
+ this.#currentTimeMsProvider.setValue(event.value as number);
169
+ break;
170
+ }
171
+ };
172
+
173
+ // Add reactive properties that depend on the targetTemporal
79
174
  @provide({ context: durationContext })
80
175
  @property({ type: Number })
81
176
  durationMs = 0;
@@ -236,39 +331,58 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
236
331
  this.#signingURL = value;
237
332
  }
238
333
 
239
- @provide({ context: playingContext })
240
334
  @property({ type: Boolean, reflect: true })
241
- playing = false;
335
+ get playing(): boolean {
336
+ return this.targetTemporal?.playbackController?.playing ?? false;
337
+ }
338
+ set playing(value: boolean) {
339
+ if (this.targetTemporal?.playbackController) {
340
+ this.targetTemporal.playbackController.setPlaying(value);
341
+ }
342
+ }
242
343
 
243
- @provide({ context: loopContext })
244
- @property({ type: Boolean, reflect: true })
245
- loop = false;
344
+ @property({ type: Boolean, reflect: true, attribute: "loop" })
345
+ get loop(): boolean {
346
+ return this.targetTemporal?.playbackController?.loop ?? this.#loop;
347
+ }
348
+ set loop(value: boolean) {
349
+ const oldValue = this.#loop;
350
+ this.#loop = value;
351
+ if (this.targetTemporal?.playbackController) {
352
+ this.targetTemporal.playbackController.setLoop(value);
353
+ }
354
+ this.requestUpdate("loop", oldValue);
355
+ }
246
356
 
247
357
  @property({ type: Boolean })
248
358
  rendering = false;
249
359
 
250
- @provide({ context: currentTimeContext })
251
360
  @property({ type: Number })
252
- currentTimeMs = Number.NaN;
253
-
254
- #FPS = 30;
255
- #MS_PER_FRAME = 1000 / this.#FPS;
361
+ get currentTimeMs(): number {
362
+ return (
363
+ this.targetTemporal?.playbackController?.currentTimeMs ?? Number.NaN
364
+ );
365
+ }
366
+ set currentTimeMs(value: number) {
367
+ if (this.targetTemporal?.playbackController) {
368
+ this.targetTemporal.playbackController.setCurrentTimeMs(value);
369
+ }
370
+ }
256
371
 
257
372
  #timegroupObserver = new MutationObserver((mutations) => {
258
373
  let shouldUpdate = false;
259
374
 
260
375
  for (const mutation of mutations) {
261
376
  if (mutation.type === "childList") {
262
- const newTimegroup = this.querySelector("ef-timegroup");
263
- if (newTimegroup !== this.targetTimegroup) {
264
- this.targetTimegroup = newTimegroup;
377
+ const newTemporal = this.findRootTemporal();
378
+ if (newTemporal !== this.targetTemporal) {
379
+ this.targetTemporal = newTemporal;
265
380
  shouldUpdate = true;
266
381
  } else if (
267
382
  mutation.target instanceof Element &&
268
- (mutation.target.tagName === "EF-TIMEGROUP" ||
269
- mutation.target.closest("ef-timegroup"))
383
+ isEFTemporal(mutation.target)
270
384
  ) {
271
- // Handle childList changes within existing timegroups
385
+ // Handle childList changes within existing temporal elements
272
386
  shouldUpdate = true;
273
387
  }
274
388
  } else if (mutation.type === "attributes") {
@@ -287,11 +401,7 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
287
401
  mutation.attributeName || "",
288
402
  ) ||
289
403
  (mutation.target instanceof Element &&
290
- (mutation.target.tagName === "EF-TIMEGROUP" ||
291
- mutation.target.tagName === "EF-VIDEO" ||
292
- mutation.target.tagName === "EF-AUDIO" ||
293
- mutation.target.tagName === "EF-CAPTIONS" ||
294
- mutation.target.closest("ef-timegroup")))
404
+ isEFTemporal(mutation.target))
295
405
  ) {
296
406
  shouldUpdate = true;
297
407
  }
@@ -302,23 +412,23 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
302
412
  // Trigger an update to ensure reactive properties recalculate
303
413
  // Use a microtask to ensure DOM updates are complete
304
414
  queueMicrotask(() => {
305
- // Recalculate duration and endTime when timegroup changes
415
+ // Recalculate duration and endTime when temporal element changes
306
416
  this.updateDurationProperties();
307
417
  this.requestUpdate();
308
- // Also ensure the targetTimegroup updates its computed properties
309
- if (this.targetTimegroup) {
310
- this.targetTimegroup.requestUpdate();
418
+ // Also ensure the targetTemporal updates its computed properties
419
+ if (this.targetTemporal) {
420
+ (this.targetTemporal as any).requestUpdate();
311
421
  }
312
422
  });
313
423
  }
314
424
  });
315
425
 
316
426
  /**
317
- * Update duration properties when timegroup changes
427
+ * Update duration properties when temporal element changes
318
428
  */
319
429
  updateDurationProperties(): void {
320
- const newDuration = this.targetTimegroup?.durationMs ?? 0;
321
- const newEndTime = this.targetTimegroup?.endTimeMs ?? 0;
430
+ const newDuration = this.targetTemporal?.durationMs ?? 0;
431
+ const newEndTime = this.targetTemporal?.endTimeMs ?? 0;
322
432
 
323
433
  if (this.durationMs !== newDuration) {
324
434
  this.durationMs = newDuration;
@@ -332,8 +442,26 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
332
442
  connectedCallback(): void {
333
443
  super.connectedCallback();
334
444
 
335
- // Initialize targetTimegroup
336
- this.targetTimegroup = this.querySelector("ef-timegroup");
445
+ // Create manual context providers for playback state
446
+ this.#playingProvider = new ContextProvider(this, {
447
+ context: playingContext,
448
+ initialValue: this.playing,
449
+ });
450
+ this.#loopProvider = new ContextProvider(this, {
451
+ context: loopContext,
452
+ initialValue: this.loop,
453
+ });
454
+ this.#currentTimeMsProvider = new ContextProvider(this, {
455
+ context: currentTimeContext,
456
+ initialValue: this.currentTimeMs,
457
+ });
458
+ this.#targetTemporalProvider = new ContextProvider(this, {
459
+ context: targetTemporalContext,
460
+ initialValue: this.targetTemporal,
461
+ });
462
+
463
+ // Initialize targetTemporal to first root temporal element
464
+ this.targetTemporal = this.findRootTemporal();
337
465
  // Initialize duration properties
338
466
  this.updateDurationProperties();
339
467
 
@@ -342,164 +470,91 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
342
470
  subtree: true,
343
471
  attributes: true,
344
472
  });
345
-
346
- if (this.playing) {
347
- this.startPlayback();
348
- }
349
473
  }
350
474
 
351
475
  disconnectedCallback(): void {
352
476
  super.disconnectedCallback();
353
477
  this.#timegroupObserver.disconnect();
354
- this.stopPlayback();
355
- // Note: Global token cache is shared across all context providers
356
- // No need to clear per-instance cache on disconnect
357
- }
358
478
 
359
- update(changedProperties: Map<string | number | symbol, unknown>) {
360
- if (changedProperties.has("playing")) {
361
- if (this.playing) {
362
- this.startPlayback();
363
- } else {
364
- this.stopPlayback();
365
- }
366
- }
367
- if (
368
- changedProperties.has("currentTimeMs") &&
369
- this.targetTimegroup &&
370
- !Number.isNaN(this.currentTimeMs)
371
- ) {
372
- if (this.targetTimegroup.currentTimeMs !== this.currentTimeMs) {
373
- if (this.isConnected) {
374
- if (this.targetTimegroup.currentTimeMs === this.currentTimeMs) {
375
- return;
376
- }
377
- this.targetTimegroup.currentTimeMs = this.currentTimeMs;
378
- }
379
- }
479
+ // Unsubscribe from controller
480
+ if (this.#targetTemporal?.playbackController) {
481
+ this.#targetTemporal.playbackController.removeListener(
482
+ this.#onControllerUpdate,
483
+ );
484
+ this.#controllerSubscribed = false;
380
485
  }
381
- super.update(changedProperties);
382
- }
383
486
 
384
- play() {
385
- this.playing = true;
487
+ this.pause();
386
488
  }
387
489
 
388
- pause() {
389
- this.playing = false;
390
- }
490
+ updated(changedProperties: Map<string | number | symbol, unknown>) {
491
+ super.updated?.(changedProperties);
391
492
 
392
- #playbackAudioContext: AudioContext | null = null;
393
- #playbackAnimationFrameRequest: number | null = null;
394
- #AUDIO_PLAYBACK_SLICE_MS = ((47 * 1024) / 48000) * 1000; // AAC-aligned: ~1002.67ms
395
-
396
- #syncPlayheadToAudioContext(target: EFTimegroup, startMs: number) {
397
- const rawTimeMs =
398
- startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1000;
399
- const nextTimeMs =
400
- Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;
401
- if (nextTimeMs !== this.currentTimeMs) {
402
- this.currentTimeMs = nextTimeMs;
403
- }
404
- this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
405
- this.#syncPlayheadToAudioContext(target, startMs);
406
- });
407
- }
493
+ // Subscribe to controller when it becomes available
494
+ if (
495
+ !this.#controllerSubscribed &&
496
+ this.#targetTemporal?.playbackController
497
+ ) {
498
+ this.#targetTemporal.playbackController.addListener(
499
+ this.#onControllerUpdate,
500
+ );
501
+ this.#controllerSubscribed = true;
408
502
 
409
- private async stopPlayback() {
410
- if (this.#playbackAudioContext) {
411
- if (this.#playbackAudioContext.state !== "closed") {
412
- await this.#playbackAudioContext.close();
503
+ // Apply stored loop value when playbackController becomes available
504
+ if (this.#loop) {
505
+ this.#targetTemporal.playbackController.setLoop(this.#loop);
413
506
  }
414
- }
415
- if (this.#playbackAnimationFrameRequest) {
416
- cancelAnimationFrame(this.#playbackAnimationFrameRequest);
417
- }
418
- this.#playbackAudioContext = null;
419
- this.#playbackAnimationFrameRequest = null;
420
- }
421
507
 
422
- private async startPlayback() {
423
- await this.stopPlayback();
424
- const timegroup = this.targetTimegroup;
425
- if (!timegroup) {
426
- return;
508
+ // Trigger initial sync of context providers
509
+ this.#playingProvider.setValue(this.playing);
510
+ this.#loopProvider.setValue(this.loop);
511
+ this.#currentTimeMsProvider.setValue(this.currentTimeMs);
427
512
  }
513
+ }
428
514
 
429
- await timegroup.waitForMediaDurations();
430
-
431
- let currentMs = timegroup.currentTimeMs;
432
- const fromMs = currentMs;
433
- const toMs = timegroup.endTimeMs;
434
-
435
- if (fromMs >= toMs) {
436
- this.pause();
437
- return;
438
- }
439
-
440
- let bufferCount = 0;
441
- this.#playbackAudioContext = new AudioContext({
442
- latencyHint: "playback",
443
- });
444
-
445
- if (this.#playbackAnimationFrameRequest) {
446
- cancelAnimationFrame(this.#playbackAnimationFrameRequest);
447
- }
448
- this.#syncPlayheadToAudioContext(timegroup, currentMs);
449
- const playbackContext = this.#playbackAudioContext;
450
- if (playbackContext.state === "suspended") {
451
- console.warn(
452
- "AudioContext is suspended, media playback will not work until user has interacted with page.",
515
+ async play() {
516
+ // If targetTemporal is not set, try to find it now
517
+ // This handles cases where the DOM may not have been fully ready during connectedCallback
518
+ if (!this.targetTemporal) {
519
+ // Wait for any temporal custom elements to be defined
520
+ const potentialTemporalTags = Array.from(this.children)
521
+ .map((el) => el.tagName.toLowerCase())
522
+ .filter((tag) => tag.startsWith("ef-"));
523
+
524
+ await Promise.all(
525
+ potentialTemporalTags.map((tag) =>
526
+ customElements.whenDefined(tag).catch(() => {}),
527
+ ),
453
528
  );
454
- this.playing = false;
455
- return;
456
- }
457
- await playbackContext.suspend();
458
529
 
459
- const fillBuffer = async () => {
460
- if (bufferCount > 2) {
530
+ const foundTemporal = this.findRootTemporal();
531
+ if (foundTemporal) {
532
+ this.targetTemporal = foundTemporal;
533
+ // Wait for it to initialize
534
+ await (foundTemporal as any).updateComplete;
535
+ } else {
536
+ console.warn("No temporal element found to play");
461
537
  return;
462
538
  }
463
- const canFillBuffer = await queueBufferSource();
464
- if (canFillBuffer) {
465
- fillBuffer();
466
- }
467
- };
539
+ }
468
540
 
469
- const queueBufferSource = async () => {
470
- if (currentMs >= toMs) {
471
- return false;
541
+ // If playbackController doesn't exist yet, wait for it
542
+ if (!this.targetTemporal.playbackController) {
543
+ await (this.targetTemporal as any).updateComplete;
544
+ // After waiting, check again
545
+ if (!this.targetTemporal.playbackController) {
546
+ console.warn("PlaybackController not available for temporal element");
547
+ return;
472
548
  }
473
- const startMs = currentMs;
474
- const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
475
- currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
476
- const audioBuffer = await timegroup.renderAudio(startMs, endMs);
477
- bufferCount++;
478
- const source = playbackContext.createBufferSource();
479
- source.buffer = audioBuffer;
480
- source.connect(playbackContext.destination);
481
- source.start((startMs - fromMs) / 1000);
482
- source.onended = () => {
483
- bufferCount--;
484
- if (endMs >= toMs) {
485
- this.pause();
486
- if (this.loop) {
487
- this.updateComplete.then(() => {
488
- this.currentTimeMs = 0;
489
- this.updateComplete.then(() => {
490
- this.play();
491
- });
492
- });
493
- }
494
- } else {
495
- fillBuffer();
496
- }
497
- };
498
- return true;
499
- };
549
+ }
550
+
551
+ this.targetTemporal.playbackController.play();
552
+ }
500
553
 
501
- await fillBuffer();
502
- await playbackContext.resume();
554
+ pause() {
555
+ if (this.targetTemporal?.playbackController) {
556
+ this.targetTemporal.playbackController.pause();
557
+ }
503
558
  }
504
559
  }
505
560