@editframe/elements 0.20.4-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 (92) hide show
  1. package/dist/DelayedLoadingState.js +0 -27
  2. package/dist/EF_FRAMEGEN.d.ts +5 -3
  3. package/dist/EF_FRAMEGEN.js +50 -11
  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.js +35 -30
  11. package/dist/elements/EFMedia/BaseMediaEngine.js +57 -73
  12. package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -76
  13. package/dist/elements/EFMedia/JitMediaEngine.js +9 -19
  14. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +3 -6
  15. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -1
  16. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +1 -1
  17. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +6 -5
  18. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
  19. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +1 -1
  20. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +1 -1
  21. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +1 -1
  22. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +4 -16
  23. package/dist/elements/EFMedia/shared/BufferUtils.js +2 -15
  24. package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
  25. package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
  26. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
  27. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -10
  28. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.d.ts +29 -0
  29. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +32 -0
  30. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +1 -15
  31. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +1 -7
  32. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -5
  33. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
  34. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -1
  35. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -70
  36. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +7 -11
  37. package/dist/elements/EFMedia.js +26 -24
  38. package/dist/elements/EFSourceMixin.js +5 -7
  39. package/dist/elements/EFSurface.js +6 -9
  40. package/dist/elements/EFTemporal.js +19 -37
  41. package/dist/elements/EFThumbnailStrip.js +16 -59
  42. package/dist/elements/EFTimegroup.js +95 -90
  43. package/dist/elements/EFVideo.d.ts +6 -2
  44. package/dist/elements/EFVideo.js +142 -107
  45. package/dist/elements/EFWaveform.js +18 -27
  46. package/dist/elements/SampleBuffer.js +2 -5
  47. package/dist/elements/TargetController.js +3 -3
  48. package/dist/elements/durationConverter.js +4 -4
  49. package/dist/elements/updateAnimations.js +14 -35
  50. package/dist/gui/ContextMixin.js +23 -52
  51. package/dist/gui/EFConfiguration.js +7 -7
  52. package/dist/gui/EFControls.js +5 -5
  53. package/dist/gui/EFFilmstrip.js +77 -98
  54. package/dist/gui/EFFitScale.js +5 -6
  55. package/dist/gui/EFFocusOverlay.js +4 -4
  56. package/dist/gui/EFPreview.js +4 -4
  57. package/dist/gui/EFScrubber.js +9 -9
  58. package/dist/gui/EFTimeDisplay.js +5 -5
  59. package/dist/gui/EFToggleLoop.js +4 -4
  60. package/dist/gui/EFTogglePlay.js +5 -5
  61. package/dist/gui/EFWorkbench.js +5 -5
  62. package/dist/gui/TWMixin2.js +1 -1
  63. package/dist/index.d.ts +1 -0
  64. package/dist/otel/BridgeSpanExporter.d.ts +13 -0
  65. package/dist/otel/BridgeSpanExporter.js +87 -0
  66. package/dist/otel/setupBrowserTracing.d.ts +12 -0
  67. package/dist/otel/setupBrowserTracing.js +30 -0
  68. package/dist/otel/tracingHelpers.d.ts +34 -0
  69. package/dist/otel/tracingHelpers.js +113 -0
  70. package/dist/transcoding/cache/RequestDeduplicator.js +0 -21
  71. package/dist/transcoding/cache/URLTokenDeduplicator.js +1 -21
  72. package/dist/transcoding/utils/UrlGenerator.js +2 -19
  73. package/dist/utils/LRUCache.js +6 -53
  74. package/package.json +10 -2
  75. package/src/elements/EFCaptions.browsertest.ts +2 -0
  76. package/src/elements/EFMedia/AssetMediaEngine.ts +65 -37
  77. package/src/elements/EFMedia/BaseMediaEngine.ts +110 -52
  78. package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
  79. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +7 -3
  80. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
  81. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +16 -10
  82. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
  83. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -116
  84. package/src/elements/EFMedia.ts +16 -1
  85. package/src/elements/EFTimegroup.browsertest.ts +10 -8
  86. package/src/elements/EFTimegroup.ts +164 -76
  87. package/src/elements/EFVideo.browsertest.ts +19 -27
  88. package/src/elements/EFVideo.ts +203 -101
  89. package/src/otel/BridgeSpanExporter.ts +150 -0
  90. package/src/otel/setupBrowserTracing.ts +68 -0
  91. package/src/otel/tracingHelpers.ts +251 -0
  92. package/types.json +1 -1
@@ -0,0 +1,76 @@
1
+ import type { BufferedSeekingInput } from "../BufferedSeekingInput";
2
+
3
+ /**
4
+ * Cache for main video BufferedSeekingInput instances
5
+ * Main video segments are typically 2s long, so we can reuse the same input
6
+ * for multiple frames within that segment (e.g., 60 frames at 30fps)
7
+ */
8
+ export class MainVideoInputCache {
9
+ private cache = new Map<string, BufferedSeekingInput>();
10
+ private maxCacheSize = 10; // Keep last 10 main inputs (covers 20 seconds at 2s/segment)
11
+
12
+ /**
13
+ * Create a cache key that uniquely identifies a segment
14
+ */
15
+ private getCacheKey(
16
+ src: string,
17
+ segmentId: number,
18
+ renditionId: string | undefined,
19
+ ): string {
20
+ return `${src}:${renditionId || "default"}:${segmentId}`;
21
+ }
22
+
23
+ /**
24
+ * Get or create BufferedSeekingInput for a main video segment
25
+ */
26
+ async getOrCreateInput(
27
+ src: string,
28
+ segmentId: number,
29
+ renditionId: string | undefined,
30
+ createInputFn: () => Promise<BufferedSeekingInput | undefined>,
31
+ ): Promise<BufferedSeekingInput | undefined> {
32
+ const cacheKey = this.getCacheKey(src, segmentId, renditionId);
33
+
34
+ // Check if we already have this segment cached
35
+ const cached = this.cache.get(cacheKey);
36
+ if (cached) {
37
+ return cached;
38
+ }
39
+
40
+ // Create new input
41
+ const input = await createInputFn();
42
+ if (!input) {
43
+ return undefined;
44
+ }
45
+
46
+ // Add to cache and maintain size limit
47
+ this.cache.set(cacheKey, input);
48
+
49
+ // Evict oldest entries if cache is too large (LRU-like behavior)
50
+ if (this.cache.size > this.maxCacheSize) {
51
+ const oldestKey = this.cache.keys().next().value;
52
+ if (oldestKey !== undefined) {
53
+ this.cache.delete(oldestKey);
54
+ }
55
+ }
56
+
57
+ return input;
58
+ }
59
+
60
+ /**
61
+ * Clear the entire cache (called when video changes)
62
+ */
63
+ clear() {
64
+ this.cache.clear();
65
+ }
66
+
67
+ /**
68
+ * Get cache statistics
69
+ */
70
+ getStats() {
71
+ return {
72
+ size: this.cache.size,
73
+ cacheKeys: Array.from(this.cache.keys()),
74
+ };
75
+ }
76
+ }
@@ -8,7 +8,7 @@ import type { InputTask } from "../shared/MediaTaskUtils";
8
8
  export const makeScrubVideoInputTask = (host: EFVideo): InputTask => {
9
9
  return new Task<
10
10
  readonly [ArrayBuffer | undefined, ArrayBuffer | undefined],
11
- BufferedSeekingInput
11
+ BufferedSeekingInput | undefined
12
12
  >(host, {
13
13
  args: () =>
14
14
  [
@@ -19,27 +19,33 @@ export const makeScrubVideoInputTask = (host: EFVideo): InputTask => {
19
19
  console.error("scrubVideoInputTask error", error);
20
20
  },
21
21
  onComplete: (_value) => {},
22
- task: async () => {
22
+ task: async (_, { signal }) => {
23
23
  const initSegment =
24
24
  await host.scrubVideoInitSegmentFetchTask.taskComplete;
25
+ if (signal.aborted) return undefined;
26
+
25
27
  const segment = await host.scrubVideoSegmentFetchTask.taskComplete;
28
+ if (signal.aborted) return undefined;
29
+
26
30
  if (!initSegment || !segment) {
27
31
  throw new Error("Scrub init segment or segment is not available");
28
32
  }
29
33
 
30
34
  // Get startTimeOffsetMs from the scrub rendition if available
31
35
  const mediaEngine = await host.mediaEngineTask.taskComplete;
36
+ if (signal.aborted) return undefined;
37
+
32
38
  const scrubRendition = mediaEngine.getScrubVideoRendition();
33
39
  const startTimeOffsetMs = scrubRendition?.startTimeOffsetMs;
34
40
 
35
- const input = new BufferedSeekingInput(
36
- await new Blob([initSegment, segment]).arrayBuffer(),
37
- {
38
- videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
39
- audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
40
- startTimeOffsetMs,
41
- },
42
- );
41
+ const arrayBuffer = await new Blob([initSegment, segment]).arrayBuffer();
42
+ if (signal.aborted) return undefined;
43
+
44
+ const input = new BufferedSeekingInput(arrayBuffer, {
45
+ videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
46
+ audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
47
+ startTimeOffsetMs,
48
+ });
43
49
  return input;
44
50
  },
45
51
  });
@@ -94,13 +94,19 @@ export const makeScrubVideoSeekTask = (host: EFVideo): ScrubVideoSeekTask => {
94
94
  return undefined;
95
95
  }
96
96
 
97
+ if (signal.aborted) {
98
+ return undefined;
99
+ }
100
+
97
101
  // Get video track and seek to precise time within the 30s scrub segment
98
102
  const videoTrack = await scrubInput.getFirstVideoTrack();
99
103
  if (!videoTrack) {
100
104
  return undefined;
101
105
  }
102
106
 
103
- signal.throwIfAborted();
107
+ if (signal.aborted) {
108
+ return undefined;
109
+ }
104
110
 
105
111
  const sample = (await scrubInput.seek(
106
112
  videoTrack.id,
@@ -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,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";
@@ -290,6 +291,20 @@ export class EFMedia extends EFTargetable(
290
291
  toMs: number,
291
292
  signal: AbortSignal = new AbortController().signal,
292
293
  ): Promise<AudioSpan | undefined> {
293
- return fetchAudioSpanningTime(this, fromMs, toMs, signal);
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
+ );
294
309
  }
295
310
  }
@@ -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