@editframe/elements 0.20.4-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 (183) hide show
  1. package/dist/DelayedLoadingState.js +0 -27
  2. package/dist/EF_FRAMEGEN.d.ts +5 -3
  3. package/dist/EF_FRAMEGEN.js +49 -11
  4. package/dist/_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js +7 -0
  5. package/dist/attachContextRoot.d.ts +1 -0
  6. package/dist/attachContextRoot.js +9 -0
  7. package/dist/elements/ContextProxiesController.d.ts +1 -2
  8. package/dist/elements/EFAudio.js +5 -9
  9. package/dist/elements/EFCaptions.d.ts +1 -3
  10. package/dist/elements/EFCaptions.js +112 -129
  11. package/dist/elements/EFImage.js +6 -7
  12. package/dist/elements/EFMedia/AssetIdMediaEngine.js +2 -5
  13. package/dist/elements/EFMedia/AssetMediaEngine.js +36 -33
  14. package/dist/elements/EFMedia/BaseMediaEngine.js +57 -73
  15. package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +1 -1
  16. package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -78
  17. package/dist/elements/EFMedia/JitMediaEngine.js +9 -19
  18. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +7 -13
  19. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +2 -3
  20. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +1 -1
  21. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +6 -5
  22. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
  23. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +1 -1
  24. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +1 -1
  25. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +1 -1
  26. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +9 -25
  27. package/dist/elements/EFMedia/shared/BufferUtils.js +2 -17
  28. package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
  29. package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
  30. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
  31. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -10
  32. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.d.ts +29 -0
  33. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +32 -0
  34. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +1 -15
  35. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +1 -7
  36. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -5
  37. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
  38. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -1
  39. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -70
  40. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +11 -18
  41. package/dist/elements/EFMedia.d.ts +19 -0
  42. package/dist/elements/EFMedia.js +44 -25
  43. package/dist/elements/EFSourceMixin.js +5 -7
  44. package/dist/elements/EFSurface.js +6 -9
  45. package/dist/elements/EFTemporal.browsertest.d.ts +11 -0
  46. package/dist/elements/EFTemporal.d.ts +10 -0
  47. package/dist/elements/EFTemporal.js +100 -41
  48. package/dist/elements/EFThumbnailStrip.js +23 -73
  49. package/dist/elements/EFTimegroup.browsertest.d.ts +3 -3
  50. package/dist/elements/EFTimegroup.d.ts +35 -14
  51. package/dist/elements/EFTimegroup.js +138 -181
  52. package/dist/elements/EFVideo.d.ts +16 -2
  53. package/dist/elements/EFVideo.js +156 -108
  54. package/dist/elements/EFWaveform.js +23 -40
  55. package/dist/elements/SampleBuffer.js +3 -7
  56. package/dist/elements/TargetController.js +5 -5
  57. package/dist/elements/durationConverter.js +4 -4
  58. package/dist/elements/renderTemporalAudio.d.ts +10 -0
  59. package/dist/elements/renderTemporalAudio.js +35 -0
  60. package/dist/elements/updateAnimations.js +19 -43
  61. package/dist/gui/ContextMixin.d.ts +5 -5
  62. package/dist/gui/ContextMixin.js +167 -162
  63. package/dist/gui/Controllable.browsertest.d.ts +0 -0
  64. package/dist/gui/Controllable.d.ts +15 -0
  65. package/dist/gui/Controllable.js +9 -0
  66. package/dist/gui/EFConfiguration.js +7 -7
  67. package/dist/gui/EFControls.browsertest.d.ts +11 -0
  68. package/dist/gui/EFControls.d.ts +18 -4
  69. package/dist/gui/EFControls.js +70 -28
  70. package/dist/gui/EFDial.browsertest.d.ts +0 -0
  71. package/dist/gui/EFDial.d.ts +18 -0
  72. package/dist/gui/EFDial.js +141 -0
  73. package/dist/gui/EFFilmstrip.browsertest.d.ts +11 -0
  74. package/dist/gui/EFFilmstrip.d.ts +12 -2
  75. package/dist/gui/EFFilmstrip.js +214 -129
  76. package/dist/gui/EFFitScale.js +5 -8
  77. package/dist/gui/EFFocusOverlay.js +4 -4
  78. package/dist/gui/EFPause.browsertest.d.ts +0 -0
  79. package/dist/gui/EFPause.d.ts +23 -0
  80. package/dist/gui/EFPause.js +59 -0
  81. package/dist/gui/EFPlay.browsertest.d.ts +0 -0
  82. package/dist/gui/EFPlay.d.ts +23 -0
  83. package/dist/gui/EFPlay.js +59 -0
  84. package/dist/gui/EFPreview.d.ts +4 -0
  85. package/dist/gui/EFPreview.js +18 -9
  86. package/dist/gui/EFResizableBox.browsertest.d.ts +0 -0
  87. package/dist/gui/EFResizableBox.d.ts +34 -0
  88. package/dist/gui/EFResizableBox.js +547 -0
  89. package/dist/gui/EFScrubber.d.ts +9 -3
  90. package/dist/gui/EFScrubber.js +13 -13
  91. package/dist/gui/EFTimeDisplay.d.ts +7 -1
  92. package/dist/gui/EFTimeDisplay.js +8 -8
  93. package/dist/gui/EFToggleLoop.d.ts +9 -3
  94. package/dist/gui/EFToggleLoop.js +7 -5
  95. package/dist/gui/EFTogglePlay.d.ts +12 -4
  96. package/dist/gui/EFTogglePlay.js +26 -21
  97. package/dist/gui/EFWorkbench.js +5 -5
  98. package/dist/gui/PlaybackController.d.ts +67 -0
  99. package/dist/gui/PlaybackController.js +310 -0
  100. package/dist/gui/TWMixin.js +1 -1
  101. package/dist/gui/TWMixin2.js +1 -1
  102. package/dist/gui/TargetOrContextMixin.d.ts +10 -0
  103. package/dist/gui/TargetOrContextMixin.js +98 -0
  104. package/dist/gui/efContext.d.ts +2 -2
  105. package/dist/index.d.ts +5 -0
  106. package/dist/index.js +5 -1
  107. package/dist/otel/BridgeSpanExporter.d.ts +13 -0
  108. package/dist/otel/BridgeSpanExporter.js +87 -0
  109. package/dist/otel/setupBrowserTracing.d.ts +12 -0
  110. package/dist/otel/setupBrowserTracing.js +32 -0
  111. package/dist/otel/tracingHelpers.d.ts +34 -0
  112. package/dist/otel/tracingHelpers.js +112 -0
  113. package/dist/style.css +1 -1
  114. package/dist/transcoding/cache/RequestDeduplicator.js +0 -21
  115. package/dist/transcoding/cache/URLTokenDeduplicator.js +1 -21
  116. package/dist/transcoding/utils/UrlGenerator.js +2 -19
  117. package/dist/utils/LRUCache.js +6 -53
  118. package/package.json +13 -5
  119. package/src/elements/ContextProxiesController.ts +10 -10
  120. package/src/elements/EFAudio.ts +1 -0
  121. package/src/elements/EFCaptions.browsertest.ts +128 -56
  122. package/src/elements/EFCaptions.ts +60 -34
  123. package/src/elements/EFImage.browsertest.ts +1 -2
  124. package/src/elements/EFMedia/AssetMediaEngine.ts +65 -37
  125. package/src/elements/EFMedia/BaseMediaEngine.ts +110 -52
  126. package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
  127. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +3 -0
  128. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +7 -3
  129. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +1 -1
  130. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
  131. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +16 -10
  132. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
  133. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -116
  134. package/src/elements/EFMedia.browsertest.ts +8 -15
  135. package/src/elements/EFMedia.ts +54 -8
  136. package/src/elements/EFSurface.browsertest.ts +2 -6
  137. package/src/elements/EFSurface.ts +1 -0
  138. package/src/elements/EFTemporal.browsertest.ts +58 -1
  139. package/src/elements/EFTemporal.ts +140 -4
  140. package/src/elements/EFThumbnailStrip.browsertest.ts +2 -8
  141. package/src/elements/EFThumbnailStrip.ts +1 -0
  142. package/src/elements/EFTimegroup.browsertest.ts +16 -15
  143. package/src/elements/EFTimegroup.ts +281 -275
  144. package/src/elements/EFVideo.browsertest.ts +162 -74
  145. package/src/elements/EFVideo.ts +229 -101
  146. package/src/elements/FetchContext.browsertest.ts +7 -2
  147. package/src/elements/TargetController.browsertest.ts +1 -0
  148. package/src/elements/TargetController.ts +1 -0
  149. package/src/elements/renderTemporalAudio.ts +108 -0
  150. package/src/elements/updateAnimations.browsertest.ts +181 -6
  151. package/src/elements/updateAnimations.ts +6 -6
  152. package/src/gui/ContextMixin.browsertest.ts +274 -27
  153. package/src/gui/ContextMixin.ts +230 -175
  154. package/src/gui/Controllable.browsertest.ts +258 -0
  155. package/src/gui/Controllable.ts +41 -0
  156. package/src/gui/EFControls.browsertest.ts +294 -80
  157. package/src/gui/EFControls.ts +139 -28
  158. package/src/gui/EFDial.browsertest.ts +84 -0
  159. package/src/gui/EFDial.ts +172 -0
  160. package/src/gui/EFFilmstrip.browsertest.ts +712 -0
  161. package/src/gui/EFFilmstrip.ts +213 -23
  162. package/src/gui/EFPause.browsertest.ts +202 -0
  163. package/src/gui/EFPause.ts +73 -0
  164. package/src/gui/EFPlay.browsertest.ts +202 -0
  165. package/src/gui/EFPlay.ts +73 -0
  166. package/src/gui/EFPreview.ts +20 -5
  167. package/src/gui/EFResizableBox.browsertest.ts +79 -0
  168. package/src/gui/EFResizableBox.ts +898 -0
  169. package/src/gui/EFScrubber.ts +7 -5
  170. package/src/gui/EFTimeDisplay.browsertest.ts +19 -19
  171. package/src/gui/EFTimeDisplay.ts +3 -1
  172. package/src/gui/EFToggleLoop.ts +6 -5
  173. package/src/gui/EFTogglePlay.ts +30 -23
  174. package/src/gui/PlaybackController.ts +522 -0
  175. package/src/gui/TWMixin.css +3 -0
  176. package/src/gui/TargetOrContextMixin.ts +185 -0
  177. package/src/gui/efContext.ts +2 -2
  178. package/src/otel/BridgeSpanExporter.ts +150 -0
  179. package/src/otel/setupBrowserTracing.ts +73 -0
  180. package/src/otel/tracingHelpers.ts +251 -0
  181. package/test/cache-integration-verification.browsertest.ts +1 -1
  182. package/types.json +1 -1
  183. package/dist/elements/ContextProxiesController.js +0 -69
@@ -1,8 +1,12 @@
1
1
  import { Task } from "@lit/task";
2
2
  import type { VideoSample } from "mediabunny";
3
+ import { withSpan } from "../../../otel/tracingHelpers.js";
3
4
  import type { VideoRendition } from "../../../transcoding/types";
5
+ import { EFMedia } from "../../EFMedia.js";
4
6
  import type { EFVideo } from "../../EFVideo";
7
+ import { BufferedSeekingInput } from "../BufferedSeekingInput.js";
5
8
  import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask";
9
+ import { MainVideoInputCache } from "./MainVideoInputCache";
6
10
  import { ScrubInputCache } from "./ScrubInputCache";
7
11
 
8
12
  type UnifiedVideoSeekTask = Task<readonly [number], VideoSample | undefined>;
@@ -10,6 +14,9 @@ type UnifiedVideoSeekTask = Task<readonly [number], VideoSample | undefined>;
10
14
  // Shared cache for scrub inputs
11
15
  const scrubInputCache = new ScrubInputCache();
12
16
 
17
+ // Shared cache for main video inputs
18
+ const mainVideoInputCache = new MainVideoInputCache();
19
+
13
20
  export const makeUnifiedVideoSeekTask = (
14
21
  host: EFVideo,
15
22
  ): UnifiedVideoSeekTask => {
@@ -22,7 +29,7 @@ export const makeUnifiedVideoSeekTask = (
22
29
  onComplete: (_value) => {},
23
30
  task: async ([desiredSeekTimeMs], { signal }) => {
24
31
  const mediaEngine = await getLatestMediaEngine(host, signal);
25
- if (!mediaEngine) return undefined;
32
+ if (!mediaEngine || signal.aborted) return undefined;
26
33
 
27
34
  // FIRST: Check if main quality content is already cached
28
35
  const mainRendition = mediaEngine.videoRendition;
@@ -35,13 +42,18 @@ export const makeUnifiedVideoSeekTask = (
35
42
  mainSegmentId !== undefined &&
36
43
  mediaEngine.isSegmentCached(mainSegmentId, mainRendition)
37
44
  ) {
38
- // Main content is cached! Use it directly.
39
- return await getMainVideoSample(
45
+ const result = await getMainVideoSample(
40
46
  host,
41
47
  mediaEngine,
42
48
  desiredSeekTimeMs,
43
49
  signal,
44
50
  );
51
+
52
+ if (signal.aborted) {
53
+ return undefined;
54
+ }
55
+
56
+ return result;
45
57
  }
46
58
  }
47
59
 
@@ -51,7 +63,12 @@ export const makeUnifiedVideoSeekTask = (
51
63
  desiredSeekTimeMs,
52
64
  signal,
53
65
  );
66
+
54
67
  if (scrubSample || signal.aborted) {
68
+ if (signal.aborted) {
69
+ return undefined;
70
+ }
71
+
55
72
  // If scrub succeeded, start background main quality upgrade (non-blocking)
56
73
  if (scrubSample) {
57
74
  startMainQualityUpgrade(
@@ -68,12 +85,18 @@ export const makeUnifiedVideoSeekTask = (
68
85
  }
69
86
 
70
87
  // THIRD: Neither are cached, fetch main video path as final fallback
71
- return await getMainVideoSample(
88
+ const result = await getMainVideoSample(
72
89
  host,
73
90
  mediaEngine,
74
91
  desiredSeekTimeMs,
75
92
  signal,
76
93
  );
94
+
95
+ if (signal.aborted) {
96
+ return undefined;
97
+ }
98
+
99
+ return result;
77
100
  },
78
101
  });
79
102
  };
@@ -86,82 +109,124 @@ async function tryGetScrubSample(
86
109
  desiredSeekTimeMs: number,
87
110
  signal: AbortSignal,
88
111
  ): Promise<VideoSample | undefined> {
89
- try {
90
- // Get scrub rendition
91
- let scrubRendition: VideoRendition | undefined;
92
-
93
- // Check if media engine has a getScrubVideoRendition method (AssetMediaEngine, etc.)
94
- if (typeof mediaEngine.getScrubVideoRendition === "function") {
95
- scrubRendition = mediaEngine.getScrubVideoRendition();
96
- } else if ("data" in mediaEngine && mediaEngine.data?.videoRenditions) {
97
- // Fallback to data structure for other engines
98
- scrubRendition = mediaEngine.data.videoRenditions.find(
99
- (r: any) => r.id === "scrub",
100
- );
101
- }
112
+ return withSpan(
113
+ "video.tryGetScrubSample",
114
+ {
115
+ desiredSeekTimeMs,
116
+ src: mediaEngine.src || "unknown",
117
+ },
118
+ undefined,
119
+ async (span) => {
120
+ try {
121
+ // Get scrub rendition
122
+ let scrubRendition: VideoRendition | undefined;
123
+
124
+ // Check if media engine has a getScrubVideoRendition method (AssetMediaEngine, etc.)
125
+ if (typeof mediaEngine.getScrubVideoRendition === "function") {
126
+ scrubRendition = mediaEngine.getScrubVideoRendition();
127
+ } else if ("data" in mediaEngine && mediaEngine.data?.videoRenditions) {
128
+ // Fallback to data structure for other engines
129
+ scrubRendition = mediaEngine.data.videoRenditions.find(
130
+ (r: any) => r.id === "scrub",
131
+ );
132
+ }
102
133
 
103
- if (!scrubRendition) return undefined;
134
+ if (!scrubRendition) {
135
+ span.setAttribute("result", "no-scrub-rendition");
136
+ return undefined;
137
+ }
104
138
 
105
- const scrubRenditionWithSrc = {
106
- ...scrubRendition,
107
- src: mediaEngine.src,
108
- };
139
+ const scrubRenditionWithSrc = {
140
+ ...scrubRendition,
141
+ src: mediaEngine.src,
142
+ };
109
143
 
110
- // Check if scrub segment is cached
111
- const segmentId = mediaEngine.computeSegmentId(
112
- desiredSeekTimeMs,
113
- scrubRenditionWithSrc,
114
- );
115
- if (segmentId === undefined) return undefined;
116
-
117
- const isCached = mediaEngine.isSegmentCached(
118
- segmentId,
119
- scrubRenditionWithSrc,
120
- );
121
- if (!isCached) return undefined; // Not cached - let main video handle it
122
-
123
- // Get cached scrub input and seek within it
124
- const scrubInput = await scrubInputCache.getOrCreateInput(
125
- segmentId,
126
- async () => {
127
- const [initSegment, mediaSegment] = await Promise.all([
128
- mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal),
129
- mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc),
130
- ]);
131
-
132
- if (!initSegment || !mediaSegment || signal.aborted) return undefined;
133
-
134
- const { BufferedSeekingInput } = await import(
135
- "../BufferedSeekingInput.js"
144
+ // Check if scrub segment is cached
145
+ const segmentId = mediaEngine.computeSegmentId(
146
+ desiredSeekTimeMs,
147
+ scrubRenditionWithSrc,
148
+ );
149
+ if (segmentId === undefined) {
150
+ span.setAttribute("result", "no-segment-id");
151
+ return undefined;
152
+ }
153
+
154
+ const isCached = mediaEngine.isSegmentCached(
155
+ segmentId,
156
+ scrubRenditionWithSrc,
136
157
  );
137
- const { EFMedia } = await import("../../EFMedia.js");
138
-
139
- return new BufferedSeekingInput(
140
- await new Blob([initSegment, mediaSegment]).arrayBuffer(),
141
- {
142
- videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
143
- audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
144
- startTimeOffsetMs: scrubRendition.startTimeOffsetMs,
158
+ span.setAttribute("isCached", isCached);
159
+ if (!isCached) {
160
+ span.setAttribute("result", "not-cached");
161
+ return undefined; // Not cached - let main video handle it
162
+ }
163
+
164
+ // Get cached scrub input and seek within it
165
+ const scrubInput = await scrubInputCache.getOrCreateInput(
166
+ segmentId,
167
+ async () => {
168
+ const [initSegment, mediaSegment] = await Promise.all([
169
+ mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal),
170
+ mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc),
171
+ ]);
172
+
173
+ if (!initSegment || !mediaSegment || signal.aborted)
174
+ return undefined;
175
+
176
+ const { BufferedSeekingInput } = await import(
177
+ "../BufferedSeekingInput.js"
178
+ );
179
+ const { EFMedia } = await import("../../EFMedia.js");
180
+
181
+ return new BufferedSeekingInput(
182
+ await new Blob([initSegment, mediaSegment]).arrayBuffer(),
183
+ {
184
+ videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
185
+ audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
186
+ startTimeOffsetMs: scrubRendition.startTimeOffsetMs,
187
+ },
188
+ );
145
189
  },
146
190
  );
147
- },
148
- );
149
191
 
150
- if (!scrubInput) return undefined;
192
+ if (!scrubInput) {
193
+ span.setAttribute("result", "no-scrub-input");
194
+ return undefined;
195
+ }
151
196
 
152
- const videoTrack = await scrubInput.getFirstVideoTrack();
153
- if (!videoTrack) return undefined;
197
+ if (signal.aborted) {
198
+ span.setAttribute("result", "aborted-after-scrub-input");
199
+ return undefined;
200
+ }
154
201
 
155
- const sample = (await scrubInput.seek(
156
- videoTrack.id,
157
- desiredSeekTimeMs,
158
- )) as unknown as VideoSample | undefined;
202
+ const videoTrack = await scrubInput.getFirstVideoTrack();
203
+ if (!videoTrack) {
204
+ span.setAttribute("result", "no-video-track");
205
+ return undefined;
206
+ }
159
207
 
160
- return sample;
161
- } catch (_error) {
162
- if (signal.aborted) return undefined;
163
- return undefined; // Scrub failed - let main video handle it
164
- }
208
+ if (signal.aborted) {
209
+ span.setAttribute("result", "aborted-after-scrub-track");
210
+ return undefined;
211
+ }
212
+
213
+ const sample = (await scrubInput.seek(
214
+ videoTrack.id,
215
+ desiredSeekTimeMs,
216
+ )) as unknown as VideoSample | undefined;
217
+
218
+ span.setAttribute("result", sample ? "success" : "no-sample");
219
+ return sample;
220
+ } catch (_error) {
221
+ if (signal.aborted) {
222
+ span.setAttribute("result", "aborted");
223
+ return undefined;
224
+ }
225
+ span.setAttribute("result", "error");
226
+ return undefined; // Scrub failed - let main video handle it
227
+ }
228
+ },
229
+ );
165
230
  }
166
231
 
167
232
  /**
@@ -173,60 +238,101 @@ async function getMainVideoSample(
173
238
  desiredSeekTimeMs: number,
174
239
  signal: AbortSignal,
175
240
  ): Promise<VideoSample | undefined> {
176
- try {
177
- // Use existing main video task chain
178
- const videoRendition = mediaEngine.getVideoRendition();
179
- if (!videoRendition) {
180
- throw new Error(
181
- "Video rendition unavailable after checking videoRendition exists",
182
- );
183
- }
184
-
185
- const segmentId = mediaEngine.computeSegmentId(
241
+ return withSpan(
242
+ "video.getMainVideoSample",
243
+ {
186
244
  desiredSeekTimeMs,
187
- videoRendition,
188
- );
189
- if (segmentId === undefined) return undefined;
190
-
191
- // Fetch main video segment
192
- const [initSegment, mediaSegment] = await Promise.all([
193
- mediaEngine.fetchInitSegment(videoRendition, signal),
194
- mediaEngine.fetchMediaSegment(segmentId, videoRendition, signal),
195
- ]);
196
-
197
- if (!initSegment || !mediaSegment) return undefined;
198
- signal.throwIfAborted();
245
+ src: mediaEngine.src || "unknown",
246
+ },
247
+ undefined,
248
+ async (span) => {
249
+ try {
250
+ // Use existing main video task chain
251
+ const videoRendition = mediaEngine.getVideoRendition();
252
+ if (!videoRendition) {
253
+ throw new Error(
254
+ "Video rendition unavailable after checking videoRendition exists",
255
+ );
256
+ }
199
257
 
200
- // Create main video input
201
- const { BufferedSeekingInput } = await import("../BufferedSeekingInput.js");
202
- const { EFMedia } = await import("../../EFMedia.js");
258
+ const segmentId = mediaEngine.computeSegmentId(
259
+ desiredSeekTimeMs,
260
+ videoRendition,
261
+ );
262
+ if (segmentId === undefined) {
263
+ span.setAttribute("result", "no-segment-id");
264
+ return undefined;
265
+ }
203
266
 
204
- const startTimeOffsetMs = videoRendition?.startTimeOffsetMs;
267
+ span.setAttribute("segmentId", segmentId);
268
+
269
+ // Get cached main video input or create new one
270
+ const mainInput = await mainVideoInputCache.getOrCreateInput(
271
+ mediaEngine.src,
272
+ segmentId,
273
+ videoRendition.id,
274
+ async () => {
275
+ // Fetch main video segment (will be cached at mediaEngine level)
276
+ const [initSegment, mediaSegment] = await Promise.all([
277
+ mediaEngine.fetchInitSegment(videoRendition, signal),
278
+ mediaEngine.fetchMediaSegment(segmentId, videoRendition, signal),
279
+ ]);
280
+
281
+ if (!initSegment || !mediaSegment) {
282
+ return undefined;
283
+ }
284
+ signal.throwIfAborted();
285
+
286
+ const startTimeOffsetMs = videoRendition?.startTimeOffsetMs;
287
+
288
+ return new BufferedSeekingInput(
289
+ await new Blob([initSegment, mediaSegment]).arrayBuffer(),
290
+ {
291
+ videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
292
+ audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
293
+ startTimeOffsetMs,
294
+ },
295
+ );
296
+ },
297
+ );
205
298
 
206
- const mainInput = new BufferedSeekingInput(
207
- await new Blob([initSegment, mediaSegment]).arrayBuffer(),
208
- {
209
- videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
210
- audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
211
- startTimeOffsetMs,
212
- },
213
- );
299
+ if (!mainInput) {
300
+ span.setAttribute("result", "no-segments");
301
+ return undefined;
302
+ }
214
303
 
215
- const videoTrack = await mainInput.getFirstVideoTrack();
216
- if (!videoTrack) return undefined;
304
+ if (signal.aborted) {
305
+ span.setAttribute("result", "aborted-after-input");
306
+ return undefined;
307
+ }
217
308
 
218
- signal.throwIfAborted();
309
+ const videoTrack = await mainInput.getFirstVideoTrack();
310
+ if (!videoTrack) {
311
+ span.setAttribute("result", "no-video-track");
312
+ return undefined;
313
+ }
219
314
 
220
- const sample = (await mainInput.seek(
221
- videoTrack.id,
222
- desiredSeekTimeMs,
223
- )) as unknown as VideoSample | undefined;
315
+ if (signal.aborted) {
316
+ span.setAttribute("result", "aborted-after-track");
317
+ return undefined;
318
+ }
224
319
 
225
- return sample;
226
- } catch (error) {
227
- if (signal.aborted) return undefined;
228
- throw error;
229
- }
320
+ const sample = (await mainInput.seek(
321
+ videoTrack.id,
322
+ desiredSeekTimeMs,
323
+ )) as unknown as VideoSample | undefined;
324
+
325
+ span.setAttribute("result", sample ? "success" : "no-sample");
326
+ return sample;
327
+ } catch (error) {
328
+ if (signal.aborted) {
329
+ span.setAttribute("result", "aborted");
330
+ return undefined;
331
+ }
332
+ throw error;
333
+ }
334
+ },
335
+ );
230
336
  }
231
337
 
232
338
  /**
@@ -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,10 @@
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";
7
+ import { withSpan } from "../otel/tracingHelpers.js";
4
8
  import type { AudioSpan } from "../transcoding/types/index.ts";
5
9
  import { UrlGenerator } from "../transcoding/utils/UrlGenerator.ts";
6
10
  import { makeAudioBufferTask } from "./EFMedia/audioTasks/makeAudioBufferTask.ts";
@@ -16,6 +20,7 @@ import { makeMediaEngineTask } from "./EFMedia/tasks/makeMediaEngineTask.ts";
16
20
  import { EFSourceMixin } from "./EFSourceMixin.js";
17
21
  import { EFTemporal } from "./EFTemporal.js";
18
22
  import { FetchMixin } from "./FetchMixin.js";
23
+ import { renderTemporalAudio } from "./renderTemporalAudio.js";
19
24
  import { EFTargetable } from "./TargetController.ts";
20
25
 
21
26
  // EF_FRAMEGEN is a global instance created in EF_FRAMEGEN.ts
@@ -46,6 +51,11 @@ export class EFMedia extends EFTargetable(
46
51
  assetType: "isobmff_files",
47
52
  }),
48
53
  ) {
54
+ @provide({ context: efContext })
55
+ get efContext(): ControllableInterface | null {
56
+ return this.rootTimegroup ?? this;
57
+ }
58
+
49
59
  // Sample buffer size configuration
50
60
  static readonly VIDEO_SAMPLE_BUFFER_SIZE = 30;
51
61
  static readonly AUDIO_SAMPLE_BUFFER_SIZE = 120;
@@ -248,13 +258,6 @@ export class EFMedia extends EFTargetable(
248
258
  }
249
259
  }
250
260
  }
251
-
252
- // if (
253
- // changedProperties.has("currentTime") ||
254
- // changedProperties.has("ownCurrentTimeMs")
255
- // ) {
256
- // updateAnimations(this);
257
- // }
258
261
  }
259
262
 
260
263
  get hasOwnDuration() {
@@ -290,6 +293,49 @@ export class EFMedia extends EFTargetable(
290
293
  toMs: number,
291
294
  signal: AbortSignal = new AbortController().signal,
292
295
  ): Promise<AudioSpan | undefined> {
293
- return fetchAudioSpanningTime(this, fromMs, toMs, signal);
296
+ return withSpan(
297
+ "media.fetchAudioSpanningTime",
298
+ {
299
+ elementId: this.id || "unknown",
300
+ tagName: this.tagName.toLowerCase(),
301
+ fromMs,
302
+ toMs,
303
+ durationMs: toMs - fromMs,
304
+ src: this.src || "none",
305
+ },
306
+ undefined,
307
+ async () => {
308
+ return fetchAudioSpanningTime(this, fromMs, toMs, signal);
309
+ },
310
+ );
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);
294
340
  }
295
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()