@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
@@ -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", () => {
@@ -1,5 +1,6 @@
1
1
  import type { TrackFragmentIndex } from "@editframe/assets";
2
2
 
3
+ import { withSpan } from "../../otel/tracingHelpers.js";
3
4
  import type {
4
5
  AudioRendition,
5
6
  InitSegmentPaths,
@@ -121,23 +122,36 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
121
122
  rendition: { trackId: number | undefined; src: string },
122
123
  signal: AbortSignal,
123
124
  ) {
124
- if (!rendition.trackId) {
125
- throw new Error(
126
- "[fetchInitSegment] Track ID is required for asset metadata",
127
- );
128
- }
129
- const url = this.buildInitSegmentUrl(rendition.trackId);
130
- const initSegment = this.data[rendition.trackId]?.initSegment;
131
- if (!initSegment) {
132
- throw new Error("Init segment not found");
133
- }
134
-
135
- // Use unified fetch method with Range headers
136
- const headers = {
137
- Range: `bytes=${initSegment.offset}-${initSegment.offset + initSegment.size - 1}`,
138
- };
139
-
140
- return this.fetchMediaWithHeaders(url, headers, signal);
125
+ return withSpan(
126
+ "assetEngine.fetchInitSegment",
127
+ {
128
+ trackId: rendition.trackId || -1,
129
+ src: rendition.src,
130
+ },
131
+ undefined,
132
+ async (span) => {
133
+ if (!rendition.trackId) {
134
+ throw new Error(
135
+ "[fetchInitSegment] Track ID is required for asset metadata",
136
+ );
137
+ }
138
+ const url = this.buildInitSegmentUrl(rendition.trackId);
139
+ const initSegment = this.data[rendition.trackId]?.initSegment;
140
+ if (!initSegment) {
141
+ throw new Error("Init segment not found");
142
+ }
143
+
144
+ span.setAttribute("offset", initSegment.offset);
145
+ span.setAttribute("size", initSegment.size);
146
+
147
+ // Use unified fetch method with Range headers
148
+ const headers = {
149
+ Range: `bytes=${initSegment.offset}-${initSegment.offset + initSegment.size - 1}`,
150
+ };
151
+
152
+ return this.fetchMediaWithHeaders(url, headers, signal);
153
+ },
154
+ );
141
155
  }
142
156
 
143
157
  async fetchMediaSegment(
@@ -145,26 +159,40 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
145
159
  rendition: { trackId: number | undefined; src: string },
146
160
  signal?: AbortSignal,
147
161
  ) {
148
- if (!rendition.trackId) {
149
- throw new Error(
150
- "[fetchMediaSegment] Track ID is required for asset metadata",
151
- );
152
- }
153
- if (segmentId === undefined) {
154
- throw new Error("Segment ID is not available");
155
- }
156
- const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);
157
- const mediaSegment = this.data[rendition.trackId]?.segments[segmentId];
158
- if (!mediaSegment) {
159
- throw new Error("Media segment not found");
160
- }
161
-
162
- // Use unified fetch method with Range headers
163
- const headers = {
164
- Range: `bytes=${mediaSegment.offset}-${mediaSegment.offset + mediaSegment.size - 1}`,
165
- };
166
-
167
- return this.fetchMediaWithHeaders(url, headers, signal);
162
+ return withSpan(
163
+ "assetEngine.fetchMediaSegment",
164
+ {
165
+ segmentId,
166
+ trackId: rendition.trackId || -1,
167
+ src: rendition.src,
168
+ },
169
+ undefined,
170
+ async (span) => {
171
+ if (!rendition.trackId) {
172
+ throw new Error(
173
+ "[fetchMediaSegment] Track ID is required for asset metadata",
174
+ );
175
+ }
176
+ if (segmentId === undefined) {
177
+ throw new Error("Segment ID is not available");
178
+ }
179
+ const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);
180
+ const mediaSegment = this.data[rendition.trackId]?.segments[segmentId];
181
+ if (!mediaSegment) {
182
+ throw new Error("Media segment not found");
183
+ }
184
+
185
+ span.setAttribute("offset", mediaSegment.offset);
186
+ span.setAttribute("size", mediaSegment.size);
187
+
188
+ // Use unified fetch method with Range headers
189
+ const headers = {
190
+ Range: `bytes=${mediaSegment.offset}-${mediaSegment.offset + mediaSegment.size - 1}`,
191
+ };
192
+
193
+ return this.fetchMediaWithHeaders(url, headers, signal);
194
+ },
195
+ );
168
196
  }
169
197
 
170
198
  /**
@@ -1,3 +1,4 @@
1
+ import { withSpan } from "../../otel/tracingHelpers.js";
1
2
  import { RequestDeduplicator } from "../../transcoding/cache/RequestDeduplicator.js";
2
3
  import type {
3
4
  AudioRendition,
@@ -61,65 +62,122 @@ export abstract class BaseMediaEngine {
61
62
  signal?: AbortSignal;
62
63
  },
63
64
  ): Promise<any> {
64
- const { responseType, headers, signal } = options;
65
-
66
- // Create cache key that includes URL and headers for proper isolation
67
- // Note: We don't include signal in cache key as it would prevent proper deduplication
68
- const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;
69
-
70
- // Check cache first
71
- const cached = mediaCache.get(cacheKey);
72
- if (cached) {
73
- // If we have a cached promise, we need to handle the caller's abort signal
74
- // without affecting the underlying request that other instances might be using
75
- if (signal) {
76
- return this.handleAbortForCachedRequest(cached, signal);
77
- }
78
- return cached;
79
- }
80
-
81
- // Use global deduplicator to prevent concurrent requests for the same resource
82
- // Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort
83
- const promise = globalRequestDeduplicator.executeRequest(
84
- cacheKey,
85
- async () => {
86
- try {
87
- // Make the fetch request WITHOUT the signal - let each caller handle their own abort
88
- // This prevents one instance's abort from affecting other instances using the shared cache
89
- const response = await this.host.fetch(url, { headers });
90
-
91
- if (responseType === "json") {
92
- return response.json();
65
+ return withSpan(
66
+ "mediaEngine.fetchWithCache",
67
+ {
68
+ url: url.length > 100 ? `${url.substring(0, 100)}...` : url,
69
+ responseType: options.responseType,
70
+ hasHeaders: !!options.headers,
71
+ },
72
+ undefined,
73
+ async (span) => {
74
+ const t0 = performance.now();
75
+ const { responseType, headers, signal } = options;
76
+
77
+ // Create cache key that includes URL and headers for proper isolation
78
+ // Note: We don't include signal in cache key as it would prevent proper deduplication
79
+ const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;
80
+
81
+ // Check cache first
82
+ const t1 = performance.now();
83
+ const cached = mediaCache.get(cacheKey);
84
+ const t2 = performance.now();
85
+ span.setAttribute("cacheLookupMs", Math.round((t2 - t1) * 1000) / 1000);
86
+
87
+ if (cached) {
88
+ span.setAttribute("cacheHit", true);
89
+ // If we have a cached promise, we need to handle the caller's abort signal
90
+ // without affecting the underlying request that other instances might be using
91
+ if (signal) {
92
+ const t3 = performance.now();
93
+ const result = await this.handleAbortForCachedRequest(
94
+ cached,
95
+ signal,
96
+ );
97
+ const t4 = performance.now();
98
+ span.setAttribute(
99
+ "handleAbortMs",
100
+ Math.round((t4 - t3) * 100) / 100,
101
+ );
102
+ span.setAttribute(
103
+ "totalCacheHitMs",
104
+ Math.round((t4 - t0) * 100) / 100,
105
+ );
106
+ return result;
93
107
  }
94
- return response.arrayBuffer();
95
- } catch (error) {
96
- // If the request was aborted, don't cache the error
108
+ span.setAttribute(
109
+ "totalCacheHitMs",
110
+ Math.round((t2 - t0) * 100) / 100,
111
+ );
112
+ return cached;
113
+ }
114
+
115
+ span.setAttribute("cacheHit", false);
116
+
117
+ // Use global deduplicator to prevent concurrent requests for the same resource
118
+ // Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort
119
+ const promise = globalRequestDeduplicator.executeRequest(
120
+ cacheKey,
121
+ async () => {
122
+ const fetchStart = performance.now();
123
+ try {
124
+ // Make the fetch request WITHOUT the signal - let each caller handle their own abort
125
+ // This prevents one instance's abort from affecting other instances using the shared cache
126
+ const response = await this.host.fetch(url, { headers });
127
+ const fetchEnd = performance.now();
128
+ span.setAttribute("fetchMs", fetchEnd - fetchStart);
129
+
130
+ if (responseType === "json") {
131
+ return response.json();
132
+ }
133
+ const buffer = await response.arrayBuffer();
134
+ span.setAttribute("sizeBytes", buffer.byteLength);
135
+ return buffer;
136
+ } catch (error) {
137
+ // If the request was aborted, don't cache the error
138
+ if (
139
+ error instanceof DOMException &&
140
+ error.name === "AbortError"
141
+ ) {
142
+ // Remove from cache so other requests can retry
143
+ mediaCache.delete(cacheKey);
144
+ }
145
+ throw error;
146
+ }
147
+ },
148
+ );
149
+
150
+ // Cache the promise (not the result) to handle concurrent requests
151
+ mediaCache.set(cacheKey, promise);
152
+
153
+ // Handle the case where the promise might be aborted
154
+ promise.catch((error) => {
155
+ // If the request was aborted, remove it from cache to prevent corrupted data
97
156
  if (error instanceof DOMException && error.name === "AbortError") {
98
- // Remove from cache so other requests can retry
99
157
  mediaCache.delete(cacheKey);
100
158
  }
101
- throw error;
159
+ });
160
+
161
+ // If the caller has a signal, handle abort logic without affecting the underlying request
162
+ if (signal) {
163
+ const result = await this.handleAbortForCachedRequest(
164
+ promise,
165
+ signal,
166
+ );
167
+ const tEnd = performance.now();
168
+ span.setAttribute(
169
+ "totalFetchMs",
170
+ Math.round((tEnd - t0) * 100) / 100,
171
+ );
172
+ return result;
102
173
  }
174
+
175
+ const result = await promise;
176
+ const tEnd = performance.now();
177
+ span.setAttribute("totalFetchMs", Math.round((tEnd - t0) * 100) / 100);
178
+ return result;
103
179
  },
104
180
  );
105
-
106
- // Cache the promise (not the result) to handle concurrent requests
107
- mediaCache.set(cacheKey, promise);
108
-
109
- // Handle the case where the promise might be aborted
110
- promise.catch((error) => {
111
- // If the request was aborted, remove it from cache to prevent corrupted data
112
- if (error instanceof DOMException && error.name === "AbortError") {
113
- mediaCache.delete(cacheKey);
114
- }
115
- });
116
-
117
- // If the caller has a signal, handle abort logic without affecting the underlying request
118
- if (signal) {
119
- return this.handleAbortForCachedRequest(promise, signal);
120
- }
121
-
122
- return promise;
123
181
  }
124
182
 
125
183
  /**