@editframe/elements 0.20.3-beta.0 → 0.21.0-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 (128) hide show
  1. package/dist/DelayedLoadingState.js +0 -27
  2. package/dist/EF_FRAMEGEN.d.ts +5 -3
  3. package/dist/EF_FRAMEGEN.js +51 -29
  4. package/dist/_virtual/_@oxc-project_runtime@0.93.0/helpers/decorate.js +7 -0
  5. package/dist/elements/ContextProxiesController.js +2 -22
  6. package/dist/elements/EFAudio.js +4 -8
  7. package/dist/elements/EFCaptions.js +59 -84
  8. package/dist/elements/EFImage.js +5 -6
  9. package/dist/elements/EFMedia/AssetIdMediaEngine.js +2 -4
  10. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +4 -4
  11. package/dist/elements/EFMedia/AssetMediaEngine.js +41 -32
  12. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +10 -2
  13. package/dist/elements/EFMedia/BaseMediaEngine.js +57 -67
  14. package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -76
  15. package/dist/elements/EFMedia/JitMediaEngine.js +22 -23
  16. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +4 -7
  17. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -3
  18. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +2 -2
  19. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +9 -7
  20. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
  21. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +2 -12
  22. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +2 -2
  23. package/dist/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.d.ts +1 -0
  24. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +6 -3
  25. package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +1 -1
  26. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +5 -17
  27. package/dist/elements/EFMedia/shared/BufferUtils.d.ts +1 -1
  28. package/dist/elements/EFMedia/shared/BufferUtils.js +2 -13
  29. package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
  30. package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +1 -1
  31. package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
  32. package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +1 -9
  33. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
  34. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +1 -2
  35. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +2 -16
  36. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.d.ts +29 -0
  37. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +32 -0
  38. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +1 -15
  39. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +3 -8
  40. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +0 -2
  41. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -7
  42. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
  43. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +0 -2
  44. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -3
  45. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -71
  46. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +8 -12
  47. package/dist/elements/EFMedia.d.ts +2 -1
  48. package/dist/elements/EFMedia.js +26 -23
  49. package/dist/elements/EFSourceMixin.js +5 -7
  50. package/dist/elements/EFSurface.js +6 -9
  51. package/dist/elements/EFTemporal.js +19 -37
  52. package/dist/elements/EFThumbnailStrip.js +16 -59
  53. package/dist/elements/EFTimegroup.js +96 -91
  54. package/dist/elements/EFVideo.d.ts +6 -2
  55. package/dist/elements/EFVideo.js +142 -107
  56. package/dist/elements/EFWaveform.js +18 -27
  57. package/dist/elements/SampleBuffer.js +2 -5
  58. package/dist/elements/TargetController.js +3 -3
  59. package/dist/elements/durationConverter.js +4 -4
  60. package/dist/elements/updateAnimations.js +14 -35
  61. package/dist/gui/ContextMixin.js +23 -52
  62. package/dist/gui/EFConfiguration.js +7 -7
  63. package/dist/gui/EFControls.js +5 -5
  64. package/dist/gui/EFFilmstrip.js +77 -98
  65. package/dist/gui/EFFitScale.js +5 -6
  66. package/dist/gui/EFFocusOverlay.js +4 -4
  67. package/dist/gui/EFPreview.js +4 -4
  68. package/dist/gui/EFScrubber.js +9 -9
  69. package/dist/gui/EFTimeDisplay.js +5 -5
  70. package/dist/gui/EFToggleLoop.js +4 -4
  71. package/dist/gui/EFTogglePlay.js +5 -5
  72. package/dist/gui/EFWorkbench.js +5 -5
  73. package/dist/gui/TWMixin2.js +1 -1
  74. package/dist/index.d.ts +1 -0
  75. package/dist/otel/BridgeSpanExporter.d.ts +13 -0
  76. package/dist/otel/BridgeSpanExporter.js +87 -0
  77. package/dist/otel/setupBrowserTracing.d.ts +12 -0
  78. package/dist/otel/setupBrowserTracing.js +30 -0
  79. package/dist/otel/tracingHelpers.d.ts +34 -0
  80. package/dist/otel/tracingHelpers.js +113 -0
  81. package/dist/transcoding/cache/RequestDeduplicator.js +0 -21
  82. package/dist/transcoding/cache/URLTokenDeduplicator.js +1 -21
  83. package/dist/transcoding/types/index.d.ts +6 -4
  84. package/dist/transcoding/utils/UrlGenerator.js +2 -19
  85. package/dist/utils/LRUCache.js +6 -53
  86. package/package.json +10 -2
  87. package/src/elements/EFCaptions.browsertest.ts +2 -0
  88. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +6 -4
  89. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +25 -23
  90. package/src/elements/EFMedia/AssetMediaEngine.ts +81 -43
  91. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +94 -0
  92. package/src/elements/EFMedia/BaseMediaEngine.ts +120 -60
  93. package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
  94. package/src/elements/EFMedia/JitMediaEngine.ts +20 -6
  95. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +5 -2
  96. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -5
  97. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +2 -1
  98. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +18 -8
  99. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +4 -16
  100. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +4 -2
  101. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +95 -0
  102. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +5 -6
  103. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +5 -4
  104. package/src/elements/EFMedia/shared/BufferUtils.ts +7 -3
  105. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +1 -1
  106. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +41 -42
  107. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -23
  108. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +1 -9
  109. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
  110. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +3 -2
  111. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -5
  112. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +17 -15
  113. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
  114. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -5
  115. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -5
  116. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -125
  117. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +2 -5
  118. package/src/elements/EFMedia.ts +18 -2
  119. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +2 -1
  120. package/src/elements/EFTimegroup.browsertest.ts +10 -8
  121. package/src/elements/EFTimegroup.ts +165 -77
  122. package/src/elements/EFVideo.browsertest.ts +19 -27
  123. package/src/elements/EFVideo.ts +203 -101
  124. package/src/otel/BridgeSpanExporter.ts +150 -0
  125. package/src/otel/setupBrowserTracing.ts +68 -0
  126. package/src/otel/tracingHelpers.ts +251 -0
  127. package/src/transcoding/types/index.ts +6 -4
  128. package/types.json +1 -1
@@ -1,9 +1,12 @@
1
1
  import { Task } from "@lit/task";
2
2
  import type { VideoSample } from "mediabunny";
3
- import { EF_RENDERING } from "../../../EF_RENDERING";
3
+ import { withSpan } from "../../../otel/tracingHelpers.js";
4
4
  import type { VideoRendition } from "../../../transcoding/types";
5
+ import { EFMedia } from "../../EFMedia.js";
5
6
  import type { EFVideo } from "../../EFVideo";
7
+ import { BufferedSeekingInput } from "../BufferedSeekingInput.js";
6
8
  import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask";
9
+ import { MainVideoInputCache } from "./MainVideoInputCache";
7
10
  import { ScrubInputCache } from "./ScrubInputCache";
8
11
 
9
12
  type UnifiedVideoSeekTask = Task<readonly [number], VideoSample | undefined>;
@@ -11,6 +14,9 @@ type UnifiedVideoSeekTask = Task<readonly [number], VideoSample | undefined>;
11
14
  // Shared cache for scrub inputs
12
15
  const scrubInputCache = new ScrubInputCache();
13
16
 
17
+ // Shared cache for main video inputs
18
+ const mainVideoInputCache = new MainVideoInputCache();
19
+
14
20
  export const makeUnifiedVideoSeekTask = (
15
21
  host: EFVideo,
16
22
  ): UnifiedVideoSeekTask => {
@@ -23,17 +29,7 @@ export const makeUnifiedVideoSeekTask = (
23
29
  onComplete: (_value) => {},
24
30
  task: async ([desiredSeekTimeMs], { signal }) => {
25
31
  const mediaEngine = await getLatestMediaEngine(host, signal);
26
- if (!mediaEngine) return undefined;
27
-
28
- // In rendering mode, skip all the scrub logic and go straight to main
29
- if (EF_RENDERING()) {
30
- return await getMainVideoSample(
31
- host,
32
- mediaEngine,
33
- desiredSeekTimeMs,
34
- signal,
35
- );
36
- }
32
+ if (!mediaEngine || signal.aborted) return undefined;
37
33
 
38
34
  // FIRST: Check if main quality content is already cached
39
35
  const mainRendition = mediaEngine.videoRendition;
@@ -46,13 +42,18 @@ export const makeUnifiedVideoSeekTask = (
46
42
  mainSegmentId !== undefined &&
47
43
  mediaEngine.isSegmentCached(mainSegmentId, mainRendition)
48
44
  ) {
49
- // Main content is cached! Use it directly.
50
- return await getMainVideoSample(
45
+ const result = await getMainVideoSample(
51
46
  host,
52
47
  mediaEngine,
53
48
  desiredSeekTimeMs,
54
49
  signal,
55
50
  );
51
+
52
+ if (signal.aborted) {
53
+ return undefined;
54
+ }
55
+
56
+ return result;
56
57
  }
57
58
  }
58
59
 
@@ -62,7 +63,12 @@ export const makeUnifiedVideoSeekTask = (
62
63
  desiredSeekTimeMs,
63
64
  signal,
64
65
  );
66
+
65
67
  if (scrubSample || signal.aborted) {
68
+ if (signal.aborted) {
69
+ return undefined;
70
+ }
71
+
66
72
  // If scrub succeeded, start background main quality upgrade (non-blocking)
67
73
  if (scrubSample) {
68
74
  startMainQualityUpgrade(
@@ -79,12 +85,18 @@ export const makeUnifiedVideoSeekTask = (
79
85
  }
80
86
 
81
87
  // THIRD: Neither are cached, fetch main video path as final fallback
82
- return await getMainVideoSample(
88
+ const result = await getMainVideoSample(
83
89
  host,
84
90
  mediaEngine,
85
91
  desiredSeekTimeMs,
86
92
  signal,
87
93
  );
94
+
95
+ if (signal.aborted) {
96
+ return undefined;
97
+ }
98
+
99
+ return result;
88
100
  },
89
101
  });
90
102
  };
@@ -97,82 +109,124 @@ async function tryGetScrubSample(
97
109
  desiredSeekTimeMs: number,
98
110
  signal: AbortSignal,
99
111
  ): Promise<VideoSample | undefined> {
100
- try {
101
- // Get scrub rendition
102
- let scrubRendition: VideoRendition | undefined;
103
-
104
- // Check if media engine has a getScrubVideoRendition method (AssetMediaEngine, etc.)
105
- if (typeof mediaEngine.getScrubVideoRendition === "function") {
106
- scrubRendition = mediaEngine.getScrubVideoRendition();
107
- } else if ("data" in mediaEngine && mediaEngine.data?.videoRenditions) {
108
- // Fallback to data structure for other engines
109
- scrubRendition = mediaEngine.data.videoRenditions.find(
110
- (r: any) => r.id === "scrub",
111
- );
112
- }
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
+ }
113
133
 
114
- if (!scrubRendition) return undefined;
134
+ if (!scrubRendition) {
135
+ span.setAttribute("result", "no-scrub-rendition");
136
+ return undefined;
137
+ }
115
138
 
116
- const scrubRenditionWithSrc = {
117
- ...scrubRendition,
118
- src: mediaEngine.src,
119
- };
139
+ const scrubRenditionWithSrc = {
140
+ ...scrubRendition,
141
+ src: mediaEngine.src,
142
+ };
120
143
 
121
- // Check if scrub segment is cached
122
- const segmentId = mediaEngine.computeSegmentId(
123
- desiredSeekTimeMs,
124
- scrubRenditionWithSrc,
125
- );
126
- if (segmentId === undefined) return undefined;
127
-
128
- const isCached = mediaEngine.isSegmentCached(
129
- segmentId,
130
- scrubRenditionWithSrc,
131
- );
132
- if (!isCached) return undefined; // Not cached - let main video handle it
133
-
134
- // Get cached scrub input and seek within it
135
- const scrubInput = await scrubInputCache.getOrCreateInput(
136
- segmentId,
137
- async () => {
138
- const [initSegment, mediaSegment] = await Promise.all([
139
- mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal),
140
- mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc),
141
- ]);
142
-
143
- if (!initSegment || !mediaSegment || signal.aborted) return undefined;
144
-
145
- const { BufferedSeekingInput } = await import(
146
- "../BufferedSeekingInput.js"
144
+ // Check if scrub segment is cached
145
+ const segmentId = mediaEngine.computeSegmentId(
146
+ desiredSeekTimeMs,
147
+ scrubRenditionWithSrc,
147
148
  );
148
- const { EFMedia } = await import("../../EFMedia.js");
149
-
150
- return new BufferedSeekingInput(
151
- await new Blob([initSegment, mediaSegment]).arrayBuffer(),
152
- {
153
- videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
154
- audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
155
- startTimeOffsetMs: scrubRendition.startTimeOffsetMs,
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,
157
+ );
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
+ );
156
189
  },
157
190
  );
158
- },
159
- );
160
191
 
161
- if (!scrubInput) return undefined;
192
+ if (!scrubInput) {
193
+ span.setAttribute("result", "no-scrub-input");
194
+ return undefined;
195
+ }
162
196
 
163
- const videoTrack = await scrubInput.getFirstVideoTrack();
164
- if (!videoTrack) return undefined;
197
+ if (signal.aborted) {
198
+ span.setAttribute("result", "aborted-after-scrub-input");
199
+ return undefined;
200
+ }
165
201
 
166
- const sample = (await scrubInput.seek(
167
- videoTrack.id,
168
- desiredSeekTimeMs,
169
- )) 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
+ }
170
207
 
171
- return sample;
172
- } catch (_error) {
173
- if (signal.aborted) return undefined;
174
- return undefined; // Scrub failed - let main video handle it
175
- }
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
+ );
176
230
  }
177
231
 
178
232
  /**
@@ -184,58 +238,101 @@ async function getMainVideoSample(
184
238
  desiredSeekTimeMs: number,
185
239
  signal: AbortSignal,
186
240
  ): Promise<VideoSample | undefined> {
187
- try {
188
- // Use existing main video task chain
189
- const segmentId = mediaEngine.computeSegmentId(
241
+ return withSpan(
242
+ "video.getMainVideoSample",
243
+ {
190
244
  desiredSeekTimeMs,
191
- mediaEngine.getVideoRendition(),
192
- );
193
- if (segmentId === undefined) return undefined;
194
-
195
- // Fetch main video segment
196
- const [initSegment, mediaSegment] = await Promise.all([
197
- mediaEngine.fetchInitSegment(mediaEngine.getVideoRendition(), signal),
198
- mediaEngine.fetchMediaSegment(
199
- segmentId,
200
- mediaEngine.getVideoRendition(),
201
- signal,
202
- ),
203
- ]);
204
-
205
- if (!initSegment || !mediaSegment) return undefined;
206
- 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
+ }
207
257
 
208
- // Create main video input
209
- const { BufferedSeekingInput } = await import("../BufferedSeekingInput.js");
210
- 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
+ }
211
266
 
212
- const videoRendition = mediaEngine.videoRendition;
213
- 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
+ );
214
298
 
215
- const mainInput = new BufferedSeekingInput(
216
- await new Blob([initSegment, mediaSegment]).arrayBuffer(),
217
- {
218
- videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
219
- audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
220
- startTimeOffsetMs,
221
- },
222
- );
299
+ if (!mainInput) {
300
+ span.setAttribute("result", "no-segments");
301
+ return undefined;
302
+ }
223
303
 
224
- const videoTrack = await mainInput.getFirstVideoTrack();
225
- if (!videoTrack) return undefined;
304
+ if (signal.aborted) {
305
+ span.setAttribute("result", "aborted-after-input");
306
+ return undefined;
307
+ }
226
308
 
227
- signal.throwIfAborted();
309
+ const videoTrack = await mainInput.getFirstVideoTrack();
310
+ if (!videoTrack) {
311
+ span.setAttribute("result", "no-video-track");
312
+ return undefined;
313
+ }
228
314
 
229
- const sample = (await mainInput.seek(
230
- videoTrack.id,
231
- desiredSeekTimeMs,
232
- )) as unknown as VideoSample | undefined;
315
+ if (signal.aborted) {
316
+ span.setAttribute("result", "aborted-after-track");
317
+ return undefined;
318
+ }
233
319
 
234
- return sample;
235
- } catch (error) {
236
- if (signal.aborted) return undefined;
237
- throw error;
238
- }
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
+ );
239
336
  }
240
337
 
241
338
  /**
@@ -9,10 +9,7 @@ import {
9
9
  type MediaBufferState,
10
10
  manageMediaBuffer,
11
11
  } from "../shared/BufferUtils";
12
- import {
13
- getLatestMediaEngine,
14
- getVideoRendition,
15
- } from "../tasks/makeMediaEngineTask";
12
+ import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask";
16
13
 
17
14
  /**
18
15
  * Configuration for video buffering - extends the generic interface
@@ -90,7 +87,7 @@ export const makeVideoBufferTask = (host: EFVideo): VideoBufferTask => {
90
87
  getRendition: async () => {
91
88
  // Get real video rendition from media engine
92
89
  const mediaEngine = await getLatestMediaEngine(host, signal);
93
- return getVideoRendition(mediaEngine);
90
+ return mediaEngine.getVideoRendition();
94
91
  },
95
92
  logError: console.error,
96
93
  },
@@ -1,6 +1,7 @@
1
1
  import { css, LitElement, type PropertyValueMap } from "lit";
2
2
  import { property, state } from "lit/decorators.js";
3
3
  import { isContextMixin } from "../gui/ContextMixin.js";
4
+ import { withSpan } from "../otel/tracingHelpers.js";
4
5
  import type { AudioSpan } from "../transcoding/types/index.ts";
5
6
  import { UrlGenerator } from "../transcoding/utils/UrlGenerator.ts";
6
7
  import { makeAudioBufferTask } from "./EFMedia/audioTasks/makeAudioBufferTask.ts";
@@ -283,12 +284,27 @@ export class EFMedia extends EFTargetable(
283
284
  /**
284
285
  * Main integration method for EFTimegroup audio playback
285
286
  * Now powered by clean, testable utility functions
287
+ * Returns undefined if no audio rendition is available
286
288
  */
287
289
  async fetchAudioSpanningTime(
288
290
  fromMs: number,
289
291
  toMs: number,
290
292
  signal: AbortSignal = new AbortController().signal,
291
- ): Promise<AudioSpan> {
292
- return fetchAudioSpanningTime(this, fromMs, toMs, signal);
293
+ ): Promise<AudioSpan | undefined> {
294
+ return withSpan(
295
+ "media.fetchAudioSpanningTime",
296
+ {
297
+ elementId: this.id || "unknown",
298
+ tagName: this.tagName.toLowerCase(),
299
+ fromMs,
300
+ toMs,
301
+ durationMs: toMs - fromMs,
302
+ src: this.src || "none",
303
+ },
304
+ undefined,
305
+ async () => {
306
+ return fetchAudioSpanningTime(this, fromMs, toMs, signal);
307
+ },
308
+ );
293
309
  }
294
310
  }
@@ -132,7 +132,8 @@ describe("MediaEngine Thumbnail Extraction", () => {
132
132
  // Get segment duration to ensure timestamps are in same segment
133
133
  const videoRendition =
134
134
  mediaEngine.getScrubVideoRendition() || mediaEngine.getVideoRendition();
135
- const segmentDurationMs = videoRendition.segmentDurationMs || 2000;
135
+ expect(videoRendition).toBeDefined();
136
+ const segmentDurationMs = videoRendition!.segmentDurationMs || 2000;
136
137
 
137
138
  // Pick timestamps within the first segment - avoid edge cases near boundaries
138
139
  const timestamps = [
@@ -616,23 +616,25 @@ describe("Dynamic content updates", () => {
616
616
  const frameTaskB = timegroup.querySelector("test-frame-task-b")!;
617
617
  const frameTaskC = timegroup.querySelector("test-frame-task-c")!;
618
618
 
619
- // following the initial update, the first frame tasks have run once.
619
+ // Following the initial update, frame tasks may run during initialization
620
620
  await timegroup.updateComplete;
621
621
 
622
- assert.equal(frameTaskA.frameTaskCount, 1);
622
+ // frameTaskB should never run (not visible at time 0ms in sequence)
623
623
  assert.equal(frameTaskB.frameTaskCount, 0);
624
- assert.equal(frameTaskC.frameTaskCount, 1);
625
624
 
626
625
  // Then we run them manually.
627
626
  await timegroup.frameTask.run();
628
627
 
629
628
  // At timeline time 0ms:
630
- // - frameTaskA (0-1000ms) should run
631
- // - frameTaskB (1000-2000ms) should NOT run
632
- // - frameTaskC (0-1000ms) should run (inherits root positioning)
633
- assert.equal(frameTaskA.frameTaskCount, 2);
629
+ // - frameTaskA (0-1000ms) should have run (visible)
630
+ // - frameTaskB (1000-2000ms) should NOT run (not visible at time 0)
631
+ // - frameTaskC (0-1000ms) should have run (inherits root positioning, visible)
632
+
633
+ // Verify visible tasks have run at least once
634
+ assert.ok(frameTaskA.frameTaskCount > 0, "frameTaskA should have run");
635
+ assert.ok(frameTaskC.frameTaskCount > 0, "frameTaskC should have run");
636
+ // Verify non-visible task has never run
634
637
  assert.equal(frameTaskB.frameTaskCount, 0); // Not visible at time 0
635
- assert.equal(frameTaskC.frameTaskCount, 2); // Nested in B but inherits root positioning
636
638
  });
637
639
  });
638
640