@editframe/elements 0.21.0-beta.0 → 0.23.6-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 +72 -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 +162 -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
@@ -36,11 +36,12 @@ export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
36
36
  css`
37
37
  :host {
38
38
  display: inline-block;
39
- white-space: pre;
40
- transform-origin: center;
39
+ white-space: normal;
40
+ line-height: 1;
41
41
  }
42
42
  :host([hidden]) {
43
- display: none;
43
+ opacity: 0;
44
+ pointer-events: none;
44
45
  }
45
46
  `,
46
47
  ];
@@ -57,7 +58,7 @@ export class EFCaptionsActiveWord extends EFTemporal(LitElement) {
57
58
  const seedValue = seed / 233; // Normalize to 0-1 range
58
59
  this.style.setProperty("--ef-word-seed", seedValue.toString());
59
60
 
60
- return html` ${this.wordText.trim()} `;
61
+ return html`${this.wordText}`;
61
62
  }
62
63
 
63
64
  @property({ type: Number, attribute: false })
@@ -98,10 +99,9 @@ export class EFCaptionsSegment extends EFTemporal(LitElement) {
98
99
  static styles = [
99
100
  css`
100
101
  :host {
101
- display: block;
102
- }
103
- :host([hidden]) {
104
- display: none;
102
+ display: inline-block;
103
+ white-space: normal;
104
+ line-height: 1;
105
105
  }
106
106
  `,
107
107
  ];
@@ -152,9 +152,11 @@ export class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {
152
152
  :host {
153
153
  display: inline-block;
154
154
  white-space: pre;
155
+ line-height: 1;
155
156
  }
156
157
  :host([hidden]) {
157
- display: none;
158
+ opacity: 0;
159
+ pointer-events: none;
158
160
  }
159
161
  `,
160
162
  ];
@@ -165,7 +167,14 @@ export class EFCaptionsBeforeActiveWord extends EFCaptionsSegment {
165
167
  return undefined;
166
168
  }
167
169
  this.hidden = false;
168
- return html` ${this.segmentText}`;
170
+
171
+ // Check if there's an active word by looking for sibling active word element
172
+ const activeWord = this.closest("ef-captions")?.querySelector(
173
+ "ef-captions-active-word",
174
+ );
175
+ const hasActiveWord = activeWord?.wordText && !activeWord.hidden;
176
+
177
+ return html`${this.segmentText}${hasActiveWord ? " " : ""}`;
169
178
  }
170
179
 
171
180
  @property({ type: Boolean, reflect: true })
@@ -205,9 +214,11 @@ export class EFCaptionsAfterActiveWord extends EFCaptionsSegment {
205
214
  :host {
206
215
  display: inline-block;
207
216
  white-space: pre;
217
+ line-height: 1;
208
218
  }
209
219
  :host([hidden]) {
210
- display: none;
220
+ opacity: 0;
221
+ pointer-events: none;
211
222
  }
212
223
  `,
213
224
  ];
@@ -218,7 +229,14 @@ export class EFCaptionsAfterActiveWord extends EFCaptionsSegment {
218
229
  return undefined;
219
230
  }
220
231
  this.hidden = false;
221
- return html`${this.segmentText} `;
232
+
233
+ // Check if there's an active word by looking for sibling active word element
234
+ const activeWord = this.closest("ef-captions")?.querySelector(
235
+ "ef-captions-active-word",
236
+ );
237
+ const hasActiveWord = activeWord?.wordText && !activeWord.hidden;
238
+
239
+ return html`${hasActiveWord ? " " : ""}${this.segmentText}`;
222
240
  }
223
241
 
224
242
  @property({ type: Boolean, reflect: true })
@@ -260,31 +278,18 @@ export class EFCaptions extends EFSourceMixin(
260
278
  css`
261
279
  :host {
262
280
  display: inline-flex;
263
- width: fit-content;
264
- align-items: baseline;
281
+ white-space: normal;
282
+ line-height: 1;
283
+ gap: 0;
265
284
  }
266
285
  ::slotted(*) {
286
+ display: inline-block;
267
287
  margin: 0;
268
288
  padding: 0;
269
289
  }
270
- ::slotted(ef-captions-active-word) {
271
- min-width: 0.5ch; /* Maintain minimum width when empty */
272
- min-height: 1em; /* Maintain height for baseline alignment */
273
- }
274
- ::slotted(ef-captions-active-word[hidden]) {
275
- opacity: 0; /* Hide when empty but maintain layout */
276
- min-width: 0.5ch;
277
- min-height: 1em;
278
- }
279
290
  `,
280
291
  ];
281
292
 
282
- @property({ type: String, attribute: "display-mode", reflect: true })
283
- displayMode: "word" | "segment" | "context" = "segment";
284
-
285
- @property({ type: Number, attribute: "context-words", reflect: true })
286
- contextWords = 3;
287
-
288
293
  @property({ type: String, attribute: "target", reflect: true })
289
294
  targetSelector = "";
290
295
 
@@ -499,9 +504,11 @@ export class EFCaptions extends EFSourceMixin(
499
504
 
500
505
  frameTask = new Task(this, {
501
506
  autoRun: EF_INTERACTIVE,
502
- args: () => [this.unifiedCaptionsDataTask.status],
507
+ args: () => [this.unifiedCaptionsDataTask.status, this.ownCurrentTimeMs],
503
508
  task: async () => {
504
509
  await this.unifiedCaptionsDataTask.taskComplete;
510
+ // Trigger updateTextContainers when data is ready or time changes
511
+ this.updateTextContainers();
505
512
  },
506
513
  });
507
514
 
@@ -519,6 +526,18 @@ export class EFCaptions extends EFSourceMixin(
519
526
  else if (this.hasCustomCaptionsData && this.rootTimegroup) {
520
527
  new CrossUpdateController(this.rootTimegroup, this);
521
528
  }
529
+
530
+ // Prevent display:none from being set on caption elements
531
+ // This maintains constant width in the parent flex container
532
+ const observer = new MutationObserver(() => {
533
+ if (this.style.display === "none") {
534
+ // Remove the display:none and use opacity instead
535
+ this.style.removeProperty("display");
536
+ this.style.opacity = "0";
537
+ this.style.pointerEvents = "none";
538
+ }
539
+ });
540
+ observer.observe(this, { attributes: true, attributeFilter: ["style"] });
522
541
  }
523
542
 
524
543
  protected updated(
@@ -557,10 +576,8 @@ export class EFCaptions extends EFSourceMixin(
557
576
  return;
558
577
  }
559
578
 
560
- // Get current time from target element or parent timegroup
561
- const currentTimeMs = this.targetElement
562
- ? this.targetElement.currentSourceTimeMs
563
- : this.ownCurrentTimeMs;
579
+ // Use ownCurrentTimeMs which is synchronized with the timegroup
580
+ const currentTimeMs = this.ownCurrentTimeMs;
564
581
  const currentTimeSec = currentTimeMs / 1000;
565
582
 
566
583
  // Find the current word from word_segments
@@ -611,6 +628,7 @@ export class EFCaptions extends EFSourceMixin(
611
628
  segmentContainer.segmentStartMs = 0;
612
629
  segmentContainer.segmentEndMs = 0;
613
630
  }
631
+ segmentContainer.requestUpdate();
614
632
  }
615
633
 
616
634
  // Process context for both word and segment cases
@@ -645,6 +663,7 @@ export class EFCaptions extends EFSourceMixin(
645
663
  container.segmentText = beforeWords;
646
664
  container.segmentStartMs = currentWord.start * 1000;
647
665
  container.segmentEndMs = currentWord.end * 1000;
666
+ container.requestUpdate();
648
667
  }
649
668
 
650
669
  // Update after containers - should be visible at the same time as active word
@@ -652,6 +671,7 @@ export class EFCaptions extends EFSourceMixin(
652
671
  container.segmentText = afterWords;
653
672
  container.segmentStartMs = currentWord.start * 1000;
654
673
  container.segmentEndMs = currentWord.end * 1000;
674
+ container.requestUpdate();
655
675
  }
656
676
  }
657
677
  } else if (currentSegment) {
@@ -673,12 +693,14 @@ export class EFCaptions extends EFSourceMixin(
673
693
  container.segmentText = ""; // Nothing before yet
674
694
  container.segmentStartMs = currentSegment.start * 1000;
675
695
  container.segmentEndMs = currentSegment.end * 1000;
696
+ container.requestUpdate();
676
697
  }
677
698
 
678
699
  for (const container of this.afterActiveWordContainers) {
679
700
  container.segmentText = allWords; // All words are upcoming
680
701
  container.segmentStartMs = currentSegment.start * 1000;
681
702
  container.segmentEndMs = currentSegment.end * 1000;
703
+ container.requestUpdate();
682
704
  }
683
705
  } else {
684
706
  // After last word ends - show all completed words in "before" container
@@ -690,12 +712,14 @@ export class EFCaptions extends EFSourceMixin(
690
712
  container.segmentText = allCompletedWords;
691
713
  container.segmentStartMs = currentSegment.start * 1000;
692
714
  container.segmentEndMs = currentSegment.end * 1000;
715
+ container.requestUpdate();
693
716
  }
694
717
 
695
718
  for (const container of this.afterActiveWordContainers) {
696
719
  container.segmentText = "";
697
720
  container.segmentStartMs = currentSegment.start * 1000;
698
721
  container.segmentEndMs = currentSegment.end * 1000;
722
+ container.requestUpdate();
699
723
  }
700
724
  }
701
725
  } else {
@@ -704,12 +728,14 @@ export class EFCaptions extends EFSourceMixin(
704
728
  container.segmentText = "";
705
729
  container.segmentStartMs = 0;
706
730
  container.segmentEndMs = 0;
731
+ container.requestUpdate();
707
732
  }
708
733
 
709
734
  for (const container of this.afterActiveWordContainers) {
710
735
  container.segmentText = "";
711
736
  container.segmentStartMs = 0;
712
737
  container.segmentEndMs = 0;
738
+ container.requestUpdate();
713
739
  }
714
740
  }
715
741
  }
@@ -6,11 +6,10 @@ import { v4 } from "uuid";
6
6
  describe("EFImage", () => {
7
7
  describe("when rendering", () => {
8
8
  beforeEach(() => {
9
- // @ts-ignore
9
+ // @ts-expect-error
10
10
  window.FRAMEGEN_BRIDGE = true;
11
11
  });
12
12
  afterEach(() => {
13
- // @ts-ignore
14
13
  delete window.FRAMEGEN_BRIDGE;
15
14
  });
16
15
  test("assetPath uses http:// protocol", () => {
@@ -55,10 +55,13 @@ const test = baseTest.extend<{
55
55
  const apiHost = `${window.location.protocol}//${window.location.host}`;
56
56
  configuration.setAttribute("api-host", apiHost);
57
57
  configuration.apiHost = apiHost;
58
+ configuration.signingURL = ""; // Disable URL signing for tests
58
59
  const host = document.createElement("ef-video");
59
60
  configuration.appendChild(host);
60
61
  host.src = "http://web:3000/head-moov-480p.mp4";
62
+ document.body.appendChild(configuration);
61
63
  await use(host);
64
+ configuration.remove();
62
65
  },
63
66
  urlGenerator: async ({}, use: any) => {
64
67
  // UrlGenerator points to integrated proxy server (same host/port as test runner)
@@ -145,7 +145,7 @@ describe("Audio Seek Task - Chunk Boundary Regression Test", () => {
145
145
 
146
146
  // Now trigger the localStorage restoration that happens in waitForMediaDurations().then()
147
147
  // This will load currentTime = 4.0 from localStorage, jumping from 0ms to 4000ms
148
- const loadedTime = timegroup.maybeLoadTimeFromLocalStorage();
148
+ const loadedTime = timegroup.loadTimeFromLocalStorage();
149
149
  if (loadedTime !== undefined) {
150
150
  timegroup.currentTime = loadedTime;
151
151
  }
@@ -1,6 +1,5 @@
1
1
  import { css } from "lit";
2
2
  import { customElement } from "lit/decorators.js";
3
- import type { VideoSample } from "mediabunny";
4
3
  import { afterEach, beforeEach, describe, vi } from "vitest";
5
4
  import { test as baseTest } from "../../test/useMSW.js";
6
5
 
@@ -106,9 +105,8 @@ describe("JIT Media Engine", () => {
106
105
  jitVideo,
107
106
  expect,
108
107
  }) => {
109
- timegroup.currentTime = 2.2;
110
- await timegroup.seekTask.taskComplete;
111
- const sample = await jitVideo.unifiedVideoSeekTask.taskComplete;
108
+ await timegroup.seek(2200);
109
+ const sample = jitVideo.unifiedVideoSeekTask.value;
112
110
  expect(sample?.timestamp).toBeCloseTo(2.2, 1);
113
111
  });
114
112
  });
@@ -148,9 +146,8 @@ describe("JIT Media Engine", () => {
148
146
  expect,
149
147
  }) => {
150
148
  await timegroup.waitForMediaDurations();
151
- timegroup.currentTimeMs = 3_000;
152
- await timegroup.seekTask.taskComplete;
153
- const frame = await jitVideo.unifiedVideoSeekTask.taskComplete;
149
+ await timegroup.seek(3000);
150
+ const frame = jitVideo.unifiedVideoSeekTask.value;
154
151
  expect(frame?.timestamp).toBeCloseTo(3, 1);
155
152
  });
156
153
 
@@ -160,9 +157,8 @@ describe("JIT Media Engine", () => {
160
157
  expect,
161
158
  }) => {
162
159
  await timegroup.waitForMediaDurations();
163
- timegroup.currentTimeMs = 5_000;
164
- await timegroup.seekTask.taskComplete;
165
- const frame = await jitVideo.unifiedVideoSeekTask.taskComplete;
160
+ await timegroup.seek(5000);
161
+ const frame = jitVideo.unifiedVideoSeekTask.value;
166
162
  expect(frame?.timestamp).toBeCloseTo(5, 1);
167
163
  });
168
164
 
@@ -172,17 +168,14 @@ describe("JIT Media Engine", () => {
172
168
  expect,
173
169
  }) => {
174
170
  await timegroup.waitForMediaDurations();
175
- timegroup.currentTimeMs = 0;
176
171
 
177
172
  // Test seeking in larger increments to avoid CI timeouts
178
173
  // while still validating incremental seeking works
179
174
  const testPoints = [0, 500, 1000, 1500, 2000, 2500, 3000];
180
- let frame: VideoSample | undefined;
181
175
 
182
176
  for (const timeMs of testPoints) {
183
- timegroup.currentTimeMs = timeMs;
184
- await timegroup.seekTask.taskComplete;
185
- frame = await jitVideo.unifiedVideoSeekTask.taskComplete;
177
+ await timegroup.seek(timeMs);
178
+ const frame = jitVideo.unifiedVideoSeekTask.value;
186
179
  expect(frame).toBeDefined();
187
180
  expect(frame?.timestamp).toBeCloseTo(timeMs / 1000, 1);
188
181
  }
@@ -1,6 +1,9 @@
1
+ import { provide } from "@lit/context";
1
2
  import { css, LitElement, type PropertyValueMap } from "lit";
2
3
  import { property, state } from "lit/decorators.js";
3
4
  import { isContextMixin } from "../gui/ContextMixin.js";
5
+ import type { ControllableInterface } from "../gui/Controllable.js";
6
+ import { efContext } from "../gui/efContext.js";
4
7
  import { withSpan } from "../otel/tracingHelpers.js";
5
8
  import type { AudioSpan } from "../transcoding/types/index.ts";
6
9
  import { UrlGenerator } from "../transcoding/utils/UrlGenerator.ts";
@@ -17,6 +20,7 @@ import { makeMediaEngineTask } from "./EFMedia/tasks/makeMediaEngineTask.ts";
17
20
  import { EFSourceMixin } from "./EFSourceMixin.js";
18
21
  import { EFTemporal } from "./EFTemporal.js";
19
22
  import { FetchMixin } from "./FetchMixin.js";
23
+ import { renderTemporalAudio } from "./renderTemporalAudio.js";
20
24
  import { EFTargetable } from "./TargetController.ts";
21
25
 
22
26
  // EF_FRAMEGEN is a global instance created in EF_FRAMEGEN.ts
@@ -47,6 +51,11 @@ export class EFMedia extends EFTargetable(
47
51
  assetType: "isobmff_files",
48
52
  }),
49
53
  ) {
54
+ @provide({ context: efContext })
55
+ get efContext(): ControllableInterface | null {
56
+ return this.rootTimegroup ?? this;
57
+ }
58
+
50
59
  // Sample buffer size configuration
51
60
  static readonly VIDEO_SAMPLE_BUFFER_SIZE = 30;
52
61
  static readonly AUDIO_SAMPLE_BUFFER_SIZE = 120;
@@ -249,13 +258,6 @@ export class EFMedia extends EFTargetable(
249
258
  }
250
259
  }
251
260
  }
252
-
253
- // if (
254
- // changedProperties.has("currentTime") ||
255
- // changedProperties.has("ownCurrentTimeMs")
256
- // ) {
257
- // updateAnimations(this);
258
- // }
259
261
  }
260
262
 
261
263
  get hasOwnDuration() {
@@ -307,4 +309,33 @@ export class EFMedia extends EFTargetable(
307
309
  },
308
310
  );
309
311
  }
312
+
313
+ /**
314
+ * Wait for media engine to load and determine duration
315
+ * Ensures media is ready for playback
316
+ */
317
+ async waitForMediaDurations(): Promise<void> {
318
+ if (this.mediaEngineTask.value) {
319
+ return;
320
+ }
321
+ await this.mediaEngineTask.run();
322
+ }
323
+
324
+ /**
325
+ * Returns media elements for playback audio rendering
326
+ * For standalone media, returns [this]; for timegroups, returns all descendants
327
+ * Used by PlaybackController for audio-driven playback
328
+ */
329
+ getMediaElements(): EFMedia[] {
330
+ return [this];
331
+ }
332
+
333
+ /**
334
+ * Render audio buffer for playback
335
+ * Called by PlaybackController during live playback
336
+ * Delegates to shared renderTemporalAudio utility for consistent behavior
337
+ */
338
+ async renderAudio(fromMs: number, toMs: number): Promise<AudioBuffer> {
339
+ return renderTemporalAudio(this, fromMs, toMs);
340
+ }
310
341
  }
@@ -26,7 +26,7 @@ const surfaceTest = baseTest.extend<{
26
26
  const container = document.createElement("div");
27
27
  render(
28
28
  html`
29
- <ef-configuration api-host="http://localhost:63315">
29
+ <ef-configuration api-host="http://localhost:63315" signing-url="">
30
30
  <ef-preview>
31
31
  <ef-timegroup id="tg" mode="sequence" class="relative h-[360px] w-[640px] overflow-hidden bg-black">
32
32
  <ef-video id="vid" src="bars-n-tone.mp4" style="width: 100%; height: 100%;"></ef-video>
@@ -38,8 +38,6 @@ const surfaceTest = baseTest.extend<{
38
38
  container,
39
39
  );
40
40
  document.body.appendChild(container);
41
- const configuration = container.querySelector("ef-configuration") as any;
42
- configuration.signingURL = "";
43
41
  const tg = container.querySelector("#tg") as EFTimegroup;
44
42
  await tg.updateComplete;
45
43
  await use(tg);
@@ -103,7 +101,7 @@ describe("EFSurface", () => {
103
101
  const container = document.createElement("div");
104
102
  render(
105
103
  html`
106
- <ef-configuration api-host="http://localhost:63315">
104
+ <ef-configuration api-host="http://localhost:63315" signing-url="">
107
105
  <ef-preview>
108
106
  <ef-timegroup mode="sequence" class="relative h-[360px] w-[640px] overflow-hidden bg-black">
109
107
  <ef-video id="v" src="bars-n-tone.mp4" style="width: 100%; height: 100%;"></ef-video>
@@ -116,8 +114,6 @@ describe("EFSurface", () => {
116
114
  container,
117
115
  );
118
116
  document.body.appendChild(container);
119
- const configuration = container.querySelector("ef-configuration") as any;
120
- configuration.signingURL = "";
121
117
  const timegroup = container.querySelector("ef-timegroup") as EFTimegroup;
122
118
  const video = container.querySelector("ef-video") as EFVideo;
123
119
  const s1 = container.querySelector("#s1") as unknown as EFSurface;
@@ -25,6 +25,7 @@ export class EFSurface extends LitElement {
25
25
  canvasRef = createRef<HTMLCanvasElement>();
26
26
 
27
27
  // @ts-expect-error controller is intentionally not referenced directly
28
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for side effects
28
29
  #targetController: TargetController = new TargetController(this);
29
30
 
30
31
  @state()
@@ -1,7 +1,9 @@
1
1
  import { LitElement } from "lit";
2
2
  import { customElement } from "lit/decorators/custom-element.js";
3
- import { describe, expect, test } from "vitest";
3
+ import { describe, expect, test, vi } from "vitest";
4
4
  import { EFTemporal } from "./EFTemporal.js";
5
+ import "./EFTimegroup.js";
6
+ import { state } from "lit/decorators.js";
5
7
 
6
8
  @customElement("ten-seconds")
7
9
  class TenSeconds extends EFTemporal(LitElement) {
@@ -156,3 +158,58 @@ describe("EFVideo sourcein attribute", () => {
156
158
  expect(element2.tagName).toBe("EF-VIDEO");
157
159
  });
158
160
  });
161
+
162
+ @customElement("test-root-lifecycle")
163
+ class TestLifecycleChild extends EFTemporal(LitElement) {
164
+ @state()
165
+ role: "root" | "child" | null = null;
166
+
167
+ didBecomeRoot() {
168
+ this.role = "root";
169
+ }
170
+ didBecomeChild() {
171
+ this.role = "child";
172
+ }
173
+ }
174
+
175
+ declare global {
176
+ interface HTMLElementTagNameMap {
177
+ "test-root-lifecycle": TestLifecycleChild;
178
+ }
179
+ }
180
+
181
+ describe("Temporal Lifecycle", () => {
182
+ test("a standalone temporal element becomes a root", async () => {
183
+ const root = document.createElement("test-root-lifecycle");
184
+ document.body.append(root);
185
+ expect(root.role).toBe("root");
186
+ });
187
+
188
+ test("temporal element in a timegroup becomes a child", async () => {
189
+ const timegroup = document.createElement("ef-timegroup");
190
+ vi.spyOn(timegroup as any, "didBecomeRoot");
191
+ vi.spyOn(timegroup as any, "didBecomeChild");
192
+ const child = document.createElement("test-root-lifecycle");
193
+ vi.spyOn(child as any, "didBecomeRoot");
194
+ vi.spyOn(child as any, "didBecomeChild");
195
+ timegroup.append(child);
196
+ document.body.append(timegroup);
197
+ expect((timegroup as any).didBecomeRoot).toHaveBeenCalledOnce();
198
+ expect((timegroup as any).didBecomeChild).not.toHaveBeenCalled();
199
+ expect((child as any).didBecomeChild).toHaveBeenCalledOnce();
200
+ });
201
+
202
+ test("timegroup nested in a timegroup becomes a child", async () => {
203
+ const timegroup = document.createElement("ef-timegroup");
204
+ const child = document.createElement("ef-timegroup");
205
+ vi.spyOn(timegroup as any, "didBecomeRoot");
206
+ vi.spyOn(timegroup as any, "didBecomeChild");
207
+ vi.spyOn(child as any, "didBecomeRoot");
208
+ vi.spyOn(child as any, "didBecomeChild");
209
+ timegroup.append(child);
210
+ document.body.append(timegroup);
211
+ expect((timegroup as any).didBecomeRoot).toHaveBeenCalledOnce();
212
+ expect((timegroup as any).didBecomeChild).not.toHaveBeenCalled();
213
+ expect((child as any).didBecomeChild).toHaveBeenCalledOnce();
214
+ });
215
+ });