@editframe/elements 0.26.3-beta.0 → 0.30.0-beta.13

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 (191) hide show
  1. package/dist/elements/EFSourceMixin.js +1 -1
  2. package/dist/elements/EFSourceMixin.js.map +1 -1
  3. package/dist/elements/EFSurface.d.ts +4 -4
  4. package/dist/elements/EFText.d.ts +52 -0
  5. package/dist/elements/EFText.js +319 -0
  6. package/dist/elements/EFText.js.map +1 -0
  7. package/dist/elements/EFTextSegment.d.ts +30 -0
  8. package/dist/elements/EFTextSegment.js +94 -0
  9. package/dist/elements/EFTextSegment.js.map +1 -0
  10. package/dist/elements/EFThumbnailStrip.d.ts +4 -4
  11. package/dist/elements/EFWaveform.d.ts +4 -4
  12. package/dist/elements/FetchMixin.js +22 -7
  13. package/dist/elements/FetchMixin.js.map +1 -1
  14. package/dist/elements/easingUtils.js +62 -0
  15. package/dist/elements/easingUtils.js.map +1 -0
  16. package/dist/elements/updateAnimations.js +57 -10
  17. package/dist/elements/updateAnimations.js.map +1 -1
  18. package/dist/gui/ContextMixin.js +11 -2
  19. package/dist/gui/ContextMixin.js.map +1 -1
  20. package/dist/gui/EFConfiguration.d.ts +4 -4
  21. package/dist/gui/EFControls.d.ts +2 -2
  22. package/dist/gui/EFDial.d.ts +4 -4
  23. package/dist/gui/EFDial.js +4 -2
  24. package/dist/gui/EFDial.js.map +1 -1
  25. package/dist/gui/EFFilmstrip.d.ts +32 -6
  26. package/dist/gui/EFFilmstrip.js +314 -50
  27. package/dist/gui/EFFilmstrip.js.map +1 -1
  28. package/dist/gui/EFFitScale.js +39 -15
  29. package/dist/gui/EFFitScale.js.map +1 -1
  30. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  31. package/dist/gui/EFPause.d.ts +4 -4
  32. package/dist/gui/EFPlay.d.ts +4 -4
  33. package/dist/gui/EFPreview.d.ts +4 -4
  34. package/dist/gui/EFPreview.js +2 -2
  35. package/dist/gui/EFPreview.js.map +1 -1
  36. package/dist/gui/EFResizableBox.d.ts +4 -4
  37. package/dist/gui/EFResizableBox.js +6 -3
  38. package/dist/gui/EFResizableBox.js.map +1 -1
  39. package/dist/gui/EFScrubber.d.ts +8 -5
  40. package/dist/gui/EFScrubber.js +64 -12
  41. package/dist/gui/EFScrubber.js.map +1 -1
  42. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  43. package/dist/gui/EFToggleLoop.d.ts +4 -4
  44. package/dist/gui/EFTogglePlay.d.ts +4 -4
  45. package/dist/gui/EFWorkbench.d.ts +4 -4
  46. package/dist/gui/EFWorkbench.js +16 -3
  47. package/dist/gui/EFWorkbench.js.map +1 -1
  48. package/dist/gui/TWMixin.js +1 -1
  49. package/dist/gui/TWMixin.js.map +1 -1
  50. package/dist/index.d.ts +3 -1
  51. package/dist/index.js +3 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/style.css +7 -120
  54. package/package.json +3 -3
  55. package/scripts/build-css.js +5 -9
  56. package/test/constants.ts +8 -0
  57. package/test/recordReplayProxyPlugin.js +76 -10
  58. package/test/setup.ts +32 -0
  59. package/test/useMSW.ts +3 -0
  60. package/test/useTranscodeMSW.ts +191 -0
  61. package/tsdown.config.ts +7 -5
  62. package/types.json +1 -1
  63. package/src/elements/ContextProxiesController.ts +0 -124
  64. package/src/elements/CrossUpdateController.ts +0 -22
  65. package/src/elements/EFAudio.browsertest.ts +0 -706
  66. package/src/elements/EFAudio.ts +0 -56
  67. package/src/elements/EFCaptions.browsertest.ts +0 -1960
  68. package/src/elements/EFCaptions.ts +0 -823
  69. package/src/elements/EFImage.browsertest.ts +0 -120
  70. package/src/elements/EFImage.ts +0 -113
  71. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
  72. package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
  73. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
  74. package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
  75. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
  76. package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
  77. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
  78. package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
  79. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
  80. package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
  81. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
  82. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
  83. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
  84. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
  85. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
  86. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
  87. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
  88. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
  89. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
  90. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
  91. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
  92. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
  93. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
  94. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
  95. package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
  96. package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
  97. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
  98. package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
  99. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
  100. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
  101. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
  102. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
  103. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
  104. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
  105. package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
  106. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
  107. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
  108. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
  109. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
  110. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
  111. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
  112. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
  113. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
  114. package/src/elements/EFMedia.browsertest.ts +0 -872
  115. package/src/elements/EFMedia.ts +0 -341
  116. package/src/elements/EFSourceMixin.ts +0 -60
  117. package/src/elements/EFSurface.browsertest.ts +0 -151
  118. package/src/elements/EFSurface.ts +0 -142
  119. package/src/elements/EFTemporal.browsertest.ts +0 -215
  120. package/src/elements/EFTemporal.ts +0 -800
  121. package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
  122. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
  123. package/src/elements/EFThumbnailStrip.ts +0 -906
  124. package/src/elements/EFTimegroup.browsertest.ts +0 -934
  125. package/src/elements/EFTimegroup.ts +0 -882
  126. package/src/elements/EFVideo.browsertest.ts +0 -1482
  127. package/src/elements/EFVideo.ts +0 -564
  128. package/src/elements/EFWaveform.ts +0 -547
  129. package/src/elements/FetchContext.browsertest.ts +0 -401
  130. package/src/elements/FetchMixin.ts +0 -38
  131. package/src/elements/SampleBuffer.ts +0 -94
  132. package/src/elements/TargetController.browsertest.ts +0 -230
  133. package/src/elements/TargetController.ts +0 -224
  134. package/src/elements/TimegroupController.ts +0 -26
  135. package/src/elements/durationConverter.ts +0 -35
  136. package/src/elements/parseTimeToMs.ts +0 -9
  137. package/src/elements/printTaskStatus.ts +0 -16
  138. package/src/elements/renderTemporalAudio.ts +0 -108
  139. package/src/elements/updateAnimations.browsertest.ts +0 -1884
  140. package/src/elements/updateAnimations.ts +0 -217
  141. package/src/elements/util.ts +0 -24
  142. package/src/gui/ContextMixin.browsertest.ts +0 -860
  143. package/src/gui/ContextMixin.ts +0 -562
  144. package/src/gui/Controllable.browsertest.ts +0 -258
  145. package/src/gui/Controllable.ts +0 -41
  146. package/src/gui/EFConfiguration.ts +0 -40
  147. package/src/gui/EFControls.browsertest.ts +0 -389
  148. package/src/gui/EFControls.ts +0 -195
  149. package/src/gui/EFDial.browsertest.ts +0 -84
  150. package/src/gui/EFDial.ts +0 -172
  151. package/src/gui/EFFilmstrip.browsertest.ts +0 -712
  152. package/src/gui/EFFilmstrip.ts +0 -1349
  153. package/src/gui/EFFitScale.ts +0 -152
  154. package/src/gui/EFFocusOverlay.ts +0 -79
  155. package/src/gui/EFPause.browsertest.ts +0 -202
  156. package/src/gui/EFPause.ts +0 -73
  157. package/src/gui/EFPlay.browsertest.ts +0 -202
  158. package/src/gui/EFPlay.ts +0 -73
  159. package/src/gui/EFPreview.ts +0 -74
  160. package/src/gui/EFResizableBox.browsertest.ts +0 -79
  161. package/src/gui/EFResizableBox.ts +0 -898
  162. package/src/gui/EFScrubber.ts +0 -151
  163. package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
  164. package/src/gui/EFTimeDisplay.ts +0 -55
  165. package/src/gui/EFToggleLoop.ts +0 -35
  166. package/src/gui/EFTogglePlay.ts +0 -70
  167. package/src/gui/EFWorkbench.ts +0 -115
  168. package/src/gui/PlaybackController.ts +0 -527
  169. package/src/gui/TWMixin.css +0 -6
  170. package/src/gui/TWMixin.ts +0 -61
  171. package/src/gui/TargetOrContextMixin.ts +0 -185
  172. package/src/gui/currentTimeContext.ts +0 -5
  173. package/src/gui/durationContext.ts +0 -3
  174. package/src/gui/efContext.ts +0 -6
  175. package/src/gui/fetchContext.ts +0 -5
  176. package/src/gui/focusContext.ts +0 -7
  177. package/src/gui/focusedElementContext.ts +0 -5
  178. package/src/gui/playingContext.ts +0 -5
  179. package/src/otel/BridgeSpanExporter.ts +0 -150
  180. package/src/otel/setupBrowserTracing.ts +0 -73
  181. package/src/otel/tracingHelpers.ts +0 -251
  182. package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
  183. package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
  184. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
  185. package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
  186. package/src/transcoding/types/index.ts +0 -312
  187. package/src/transcoding/utils/MediaUtils.ts +0 -63
  188. package/src/transcoding/utils/UrlGenerator.ts +0 -68
  189. package/src/transcoding/utils/constants.ts +0 -36
  190. package/src/utils/LRUCache.test.ts +0 -274
  191. package/src/utils/LRUCache.ts +0 -696
@@ -1,430 +0,0 @@
1
- import {
2
- AudioSampleSink,
3
- BufferSource,
4
- Input,
5
- InputAudioTrack,
6
- type InputTrack,
7
- InputVideoTrack,
8
- MP4,
9
- VideoSampleSink,
10
- } from "mediabunny";
11
- import { withSpan } from "../../otel/tracingHelpers.js";
12
- import { type MediaSample, SampleBuffer } from "../SampleBuffer";
13
- import { roundToMilliseconds } from "./shared/PrecisionUtils";
14
-
15
- interface BufferedSeekingInputOptions {
16
- videoBufferSize?: number;
17
- audioBufferSize?: number;
18
- /**
19
- * Timeline offset in milliseconds to map user timeline to media timeline.
20
- * Applied during seeking to handle media that doesn't start at 0ms.
21
- */
22
- startTimeOffsetMs?: number;
23
- }
24
-
25
- const defaultOptions: BufferedSeekingInputOptions = {
26
- videoBufferSize: 30,
27
- audioBufferSize: 100,
28
- startTimeOffsetMs: 0,
29
- };
30
-
31
- export class NoSample extends RangeError {}
32
-
33
- export class ConcurrentSeekError extends RangeError {}
34
-
35
- export class BufferedSeekingInput {
36
- private input: Input;
37
- private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();
38
- private trackBuffers: Map<number, SampleBuffer> = new Map();
39
- private options: BufferedSeekingInputOptions;
40
- // Separate locks for different operation types to prevent unnecessary blocking
41
- private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();
42
- private trackSeekPromises: Map<number, Promise<any>> = new Map();
43
-
44
- /**
45
- * Timeline offset in milliseconds to map user timeline to media timeline.
46
- * Applied during seeking to handle media that doesn't start at 0ms.
47
- */
48
- private readonly startTimeOffsetMs: number;
49
-
50
- constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {
51
- const bufferSource = new BufferSource(arrayBuffer);
52
- const input = new Input({
53
- source: bufferSource,
54
- formats: [MP4],
55
- });
56
- this.input = input;
57
- this.options = { ...defaultOptions, ...options };
58
- this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;
59
- }
60
-
61
- // Buffer inspection API for testing
62
- getBufferSize(trackId: number): number {
63
- const buffer = this.trackBuffers.get(trackId);
64
- return buffer ? buffer.length : 0;
65
- }
66
-
67
- getBufferContents(trackId: number): readonly MediaSample[] {
68
- const buffer = this.trackBuffers.get(trackId);
69
- return buffer ? Object.freeze([...buffer.getContents()]) : [];
70
- }
71
-
72
- getBufferTimestamps(trackId: number): number[] {
73
- const contents = this.getBufferContents(trackId);
74
- return contents.map((sample) => sample.timestamp || 0);
75
- }
76
-
77
- clearBuffer(trackId: number): void {
78
- const buffer = this.trackBuffers.get(trackId);
79
- if (buffer) {
80
- buffer.clear();
81
- }
82
- }
83
-
84
- computeDuration() {
85
- return this.input.computeDuration();
86
- }
87
-
88
- async getTrack(trackId: number) {
89
- const tracks = await this.input.getTracks();
90
- const track = tracks.find((track) => track.id === trackId);
91
- if (!track) {
92
- throw new Error(`Track ${trackId} not found`);
93
- }
94
- return track;
95
- }
96
-
97
- async getAudioTrack(trackId: number) {
98
- const tracks = await this.input.getAudioTracks();
99
- const track = tracks.find(
100
- (track) => track.id === trackId && track.type === "audio",
101
- );
102
- if (!track) {
103
- throw new Error(`Track ${trackId} not found`);
104
- }
105
- return track;
106
- }
107
-
108
- async getVideoTrack(trackId: number) {
109
- const tracks = await this.input.getVideoTracks();
110
- const track = tracks.find(
111
- (track) => track.id === trackId && track.type === "video",
112
- );
113
- if (!track) {
114
- throw new Error(`Track ${trackId} not found`);
115
- }
116
- return track;
117
- }
118
-
119
- async getFirstVideoTrack() {
120
- const tracks = await this.input.getVideoTracks();
121
- return tracks[0];
122
- }
123
-
124
- async getFirstAudioTrack() {
125
- const tracks = await this.input.getAudioTracks();
126
- return tracks[0];
127
- }
128
-
129
- getTrackIterator(track: InputTrack) {
130
- if (this.trackIterators.has(track.id)) {
131
- // biome-ignore lint/style/noNonNullAssertion: we know the map has the key
132
- return this.trackIterators.get(track.id)!;
133
- }
134
-
135
- const trackIterator = this.createTrackIterator(track);
136
-
137
- this.trackIterators.set(track.id, trackIterator);
138
-
139
- return trackIterator;
140
- }
141
-
142
- createTrackSampleSink(track: InputTrack) {
143
- if (track instanceof InputAudioTrack) {
144
- return new AudioSampleSink(track);
145
- }
146
- if (track instanceof InputVideoTrack) {
147
- return new VideoSampleSink(track);
148
- }
149
- throw new Error(`Unsupported track type ${track.type}`);
150
- }
151
-
152
- createTrackIterator(track: InputTrack) {
153
- const sampleSink = this.createTrackSampleSink(track);
154
- return sampleSink.samples();
155
- }
156
-
157
- createTrackBuffer(track: InputTrack) {
158
- if (track.type === "audio") {
159
- const bufferSize = this.options.audioBufferSize;
160
- const sampleBuffer = new SampleBuffer(bufferSize);
161
- return sampleBuffer;
162
- }
163
- const bufferSize = this.options.videoBufferSize;
164
- const sampleBuffer = new SampleBuffer(bufferSize);
165
- return sampleBuffer;
166
- }
167
-
168
- getTrackBuffer(track: InputTrack) {
169
- const maybeTrackBuffer = this.trackBuffers.get(track.id);
170
-
171
- if (maybeTrackBuffer) {
172
- return maybeTrackBuffer;
173
- }
174
-
175
- const trackBuffer = this.createTrackBuffer(track);
176
- this.trackBuffers.set(track.id, trackBuffer);
177
- return trackBuffer;
178
- }
179
-
180
- async seek(trackId: number, timeMs: number) {
181
- return withSpan(
182
- "bufferedInput.seek",
183
- {
184
- trackId,
185
- timeMs,
186
- startTimeOffsetMs: this.startTimeOffsetMs,
187
- },
188
- undefined,
189
- async (span) => {
190
- // Apply timeline offset to map user timeline to media timeline
191
- const mediaTimeMs = timeMs + this.startTimeOffsetMs;
192
-
193
- // Round using consistent precision handling
194
- const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);
195
- span.setAttribute("roundedMediaTimeMs", roundedMediaTimeMs);
196
-
197
- // Serialize seek operations per track (but don't block iterator creation)
198
- const existingSeek = this.trackSeekPromises.get(trackId);
199
- if (existingSeek) {
200
- span.setAttribute("waitedForExistingSeek", true);
201
- await existingSeek;
202
- }
203
-
204
- const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);
205
- this.trackSeekPromises.set(trackId, seekPromise);
206
-
207
- try {
208
- return await seekPromise;
209
- } finally {
210
- this.trackSeekPromises.delete(trackId);
211
- }
212
- },
213
- );
214
- }
215
-
216
- private async resetIterator(track: InputTrack) {
217
- const trackBuffer = this.trackBuffers.get(track.id);
218
- trackBuffer?.clear();
219
- // Clean up iterator safely - wait for any ongoing iterator creation
220
- const ongoingIteratorCreation = this.trackIteratorCreationPromises.get(
221
- track.id,
222
- );
223
- if (ongoingIteratorCreation) {
224
- await ongoingIteratorCreation;
225
- }
226
-
227
- const iterator = this.trackIterators.get(track.id);
228
- if (iterator) {
229
- try {
230
- await iterator.return?.();
231
- } catch (_error) {
232
- // Iterator cleanup failed, continue anyway
233
- }
234
- }
235
- this.trackIterators.delete(track.id);
236
- }
237
-
238
- #seekLock?: PromiseWithResolvers<void>;
239
-
240
- private async seekSafe(trackId: number, timeMs: number) {
241
- return withSpan(
242
- "bufferedInput.seekSafe",
243
- {
244
- trackId,
245
- timeMs,
246
- },
247
- undefined,
248
- async (span) => {
249
- if (this.#seekLock) {
250
- span.setAttribute("waitedForSeekLock", true);
251
- await this.#seekLock.promise;
252
- }
253
- const seekLock = Promise.withResolvers<void>();
254
- this.#seekLock = seekLock;
255
-
256
- try {
257
- const track = await this.getTrack(trackId);
258
- span.setAttribute("trackType", track.type);
259
-
260
- const trackBuffer = this.getTrackBuffer(track);
261
-
262
- const roundedTimeMs = roundToMilliseconds(timeMs);
263
- const firstTimestampMs = roundToMilliseconds(
264
- (await track.getFirstTimestamp()) * 1000,
265
- );
266
- span.setAttribute("firstTimestampMs", firstTimestampMs);
267
-
268
- if (roundedTimeMs < firstTimestampMs) {
269
- console.error("Seeking outside bounds of input", {
270
- roundedTimeMs,
271
- firstTimestampMs,
272
- });
273
- throw new NoSample(
274
- `Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`,
275
- );
276
- }
277
-
278
- // Check if we need to reset iterator for seeks outside current buffer range
279
- const bufferContents = trackBuffer.getContents();
280
- span.setAttribute("bufferContentsLength", bufferContents.length);
281
-
282
- if (bufferContents.length > 0) {
283
- const bufferStartMs = roundToMilliseconds(
284
- trackBuffer.firstTimestamp * 1000,
285
- );
286
- span.setAttribute("bufferStartMs", bufferStartMs);
287
-
288
- if (roundedTimeMs < bufferStartMs) {
289
- span.setAttribute("resetIterator", true);
290
- await this.resetIterator(track);
291
- }
292
- }
293
-
294
- const alreadyInBuffer = trackBuffer.find(timeMs);
295
- if (alreadyInBuffer) {
296
- span.setAttribute("foundInBuffer", true);
297
- span.setAttribute("bufferSize", trackBuffer.length);
298
- const contents = trackBuffer.getContents();
299
- if (contents.length > 0) {
300
- span.setAttribute(
301
- "bufferTimestamps",
302
- contents
303
- .map((s) => Math.round((s.timestamp || 0) * 1000))
304
- .slice(0, 10)
305
- .join(","),
306
- );
307
- }
308
- return alreadyInBuffer;
309
- }
310
-
311
- // Buffer miss - record buffer state
312
- span.setAttribute("foundInBuffer", false);
313
- span.setAttribute("bufferSize", trackBuffer.length);
314
- span.setAttribute("requestedTimeMs", Math.round(timeMs));
315
-
316
- const contents = trackBuffer.getContents();
317
- if (contents.length > 0) {
318
- const firstSample = contents[0];
319
- const lastSample = contents[contents.length - 1];
320
- if (firstSample && lastSample) {
321
- const bufferStartMs = Math.round(
322
- (firstSample.timestamp || 0) * 1000,
323
- );
324
- const bufferEndMs = Math.round(
325
- ((lastSample.timestamp || 0) + (lastSample.duration || 0)) *
326
- 1000,
327
- );
328
- span.setAttribute("bufferStartMs", bufferStartMs);
329
- span.setAttribute("bufferEndMs", bufferEndMs);
330
- span.setAttribute(
331
- "bufferRangeMs",
332
- `${bufferStartMs}-${bufferEndMs}`,
333
- );
334
- }
335
- }
336
-
337
- const iterator = this.getTrackIterator(track);
338
- let iterationCount = 0;
339
- const decodeStart = performance.now();
340
-
341
- while (true) {
342
- iterationCount++;
343
- const iterStart = performance.now();
344
- const { done, value: decodedSample } = await iterator.next();
345
- const iterEnd = performance.now();
346
-
347
- // Record individual iteration timing for first 5 iterations
348
- if (iterationCount <= 5) {
349
- span.setAttribute(
350
- `iter${iterationCount}Ms`,
351
- Math.round((iterEnd - iterStart) * 100) / 100,
352
- );
353
- }
354
-
355
- if (decodedSample) {
356
- trackBuffer.push(decodedSample);
357
- if (iterationCount <= 5) {
358
- span.setAttribute(
359
- `iter${iterationCount}Timestamp`,
360
- Math.round((decodedSample.timestamp || 0) * 1000),
361
- );
362
- }
363
- }
364
-
365
- const foundSample = trackBuffer.find(roundedTimeMs);
366
- if (foundSample) {
367
- const decodeEnd = performance.now();
368
- span.setAttribute("iterationCount", iterationCount);
369
- span.setAttribute(
370
- "decodeMs",
371
- Math.round((decodeEnd - decodeStart) * 100) / 100,
372
- );
373
- span.setAttribute(
374
- "avgIterMs",
375
- Math.round(((decodeEnd - decodeStart) / iterationCount) * 100) /
376
- 100,
377
- );
378
- span.setAttribute("foundSample", true);
379
- span.setAttribute(
380
- "foundTimestamp",
381
- Math.round((foundSample.timestamp || 0) * 1000),
382
- );
383
- return foundSample;
384
- }
385
- if (done) {
386
- break;
387
- }
388
- }
389
-
390
- span.setAttribute("iterationCount", iterationCount);
391
- span.setAttribute("reachedEnd", true);
392
-
393
- // Check if we're seeking to the exact end of the track (legitimate use case)
394
- const finalBufferContents = trackBuffer.getContents();
395
- if (finalBufferContents.length > 0) {
396
- const lastSample =
397
- finalBufferContents[finalBufferContents.length - 1];
398
- const lastSampleEndMs = roundToMilliseconds(
399
- ((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) *
400
- 1000,
401
- );
402
-
403
- // Only return last sample if seeking to exactly the track duration
404
- // (end of video) AND we have the final segment loaded
405
- const trackDurationMs = (await track.computeDuration()) * 1000;
406
- const isSeekingToTrackEnd =
407
- roundToMilliseconds(timeMs) ===
408
- roundToMilliseconds(trackDurationMs);
409
- const isAtEndOfTrack =
410
- roundToMilliseconds(timeMs) >= lastSampleEndMs;
411
-
412
- if (isSeekingToTrackEnd && isAtEndOfTrack) {
413
- span.setAttribute("returnedLastSample", true);
414
- return lastSample;
415
- }
416
- }
417
-
418
- // For all other cases (seeking within track but outside buffer range), throw error
419
- // The caller should ensure the correct segment is loaded before seeking
420
- throw new NoSample(
421
- `Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,
422
- );
423
- } finally {
424
- this.#seekLock = undefined;
425
- seekLock.resolve();
426
- }
427
- },
428
- );
429
- }
430
- }
@@ -1,226 +0,0 @@
1
- import { describe } from "vitest";
2
- import { test as baseTest } from "../../../test/useMSW.js";
3
-
4
- import type { ManifestResponse } from "../../transcoding/types/index.js";
5
- import { UrlGenerator } from "../../transcoding/utils/UrlGenerator";
6
- import "../EFVideo.js";
7
- import type { EFVideo } from "../EFVideo.js";
8
- import { JitMediaEngine } from "./JitMediaEngine";
9
-
10
- const test = baseTest.extend<{
11
- emptyManifestResponse: ManifestResponse;
12
- urlGenerator: UrlGenerator;
13
- manifestUrl: string;
14
- mediaEngine: JitMediaEngine;
15
- abortSignal: AbortSignal;
16
- testUrl: string;
17
- host: EFVideo;
18
- }>({
19
- mediaEngine: async ({ manifestUrl, urlGenerator, host }, use: any) => {
20
- const engine = await JitMediaEngine.fetch(host, urlGenerator, manifestUrl);
21
- await use(engine);
22
- },
23
- manifestUrl: async ({ urlGenerator, host }, use: any) => {
24
- const url = urlGenerator.generateManifestUrl(host.src);
25
- await use(url);
26
- },
27
-
28
- emptyManifestResponse: async ({}, use: any) => {
29
- const emptyResponse: ManifestResponse = {
30
- version: "1.0",
31
- type: "cmaf",
32
- duration: 60,
33
- durationMs: 60000,
34
- segmentDuration: 4000,
35
- baseUrl: "http://api.example.com/",
36
- sourceUrl: "http://example.com/video.mp4",
37
- audioRenditions: [],
38
- videoRenditions: [],
39
- endpoints: {
40
- initSegment: "http://api.example.com/init/{renditionId}",
41
- mediaSegment:
42
- "http://api.example.com/segment/{segmentId}/{renditionId}",
43
- },
44
- jitInfo: {
45
- parallelTranscodingSupported: true,
46
- expectedTranscodeLatency: 1000,
47
- segmentCount: 15,
48
- },
49
- };
50
- await use(emptyResponse);
51
- },
52
- host: async ({}, use: any) => {
53
- const configuration = document.createElement("ef-configuration");
54
- // Use integrated proxy server (same host/port as test runner)
55
- const apiHost = `${window.location.protocol}//${window.location.host}`;
56
- configuration.setAttribute("api-host", apiHost);
57
- configuration.apiHost = apiHost;
58
- configuration.signingURL = ""; // Disable URL signing for tests
59
- const host = document.createElement("ef-video");
60
- configuration.appendChild(host);
61
- host.src = "http://web:3000/head-moov-480p.mp4";
62
- document.body.appendChild(configuration);
63
- await use(host);
64
- configuration.remove();
65
- },
66
- urlGenerator: async ({}, use: any) => {
67
- // UrlGenerator points to integrated proxy server (same host/port as test runner)
68
- const apiHost = `${window.location.protocol}//${window.location.host}`;
69
- const generator = new UrlGenerator(() => apiHost);
70
- await use(generator);
71
- },
72
-
73
- abortSignal: async ({}, use: any) => {
74
- const signal = new AbortController().signal;
75
- await use(signal);
76
- },
77
- testUrl: async ({}, use: any) => {
78
- const url = "http://api.example.com/manifest";
79
- await use(url);
80
- },
81
- });
82
-
83
- describe("JitMediaEngine", () => {
84
- test("provides duration from manifest data", async ({
85
- mediaEngine,
86
- expect,
87
- }) => {
88
- expect(mediaEngine.durationMs).toBe(10000);
89
- });
90
-
91
- test("provides source URL from manifest data", async ({
92
- mediaEngine,
93
- host,
94
- expect,
95
- }) => {
96
- expect(mediaEngine.src).toBe(host.src);
97
- });
98
-
99
- test("returns audio rendition with correct properties", ({
100
- mediaEngine,
101
- host,
102
- expect,
103
- }) => {
104
- const audioRendition = mediaEngine.audioRendition;
105
-
106
- expect(audioRendition).toBeDefined();
107
- expect(audioRendition!.id).toBe("audio");
108
- expect(audioRendition!.trackId).toBeUndefined();
109
- expect(audioRendition!.src).toBe(host.src);
110
- expect(audioRendition!.segmentDurationMs).toBe(2000);
111
- });
112
-
113
- test("returns undefined audio rendition when none available", ({
114
- urlGenerator,
115
- host,
116
- expect,
117
- }) => {
118
- const engine = new JitMediaEngine(host, urlGenerator);
119
-
120
- expect(engine.audioRendition).toBeUndefined();
121
- });
122
-
123
- test("returns video rendition with correct properties", ({
124
- mediaEngine,
125
- host,
126
- expect,
127
- }) => {
128
- const videoRendition = mediaEngine.videoRendition;
129
-
130
- expect(videoRendition).toBeDefined();
131
- expect(videoRendition!.id).toBe("high");
132
- expect(videoRendition!.trackId).toBeUndefined();
133
- expect(videoRendition!.src).toBe(host.src);
134
- expect(videoRendition!.segmentDurationMs).toBe(2000);
135
- });
136
-
137
- test("returns undefined video rendition when none available", ({
138
- urlGenerator,
139
- host,
140
- expect,
141
- }) => {
142
- const engine = new JitMediaEngine(host, urlGenerator);
143
-
144
- expect(engine.videoRendition).toBeUndefined();
145
- });
146
-
147
- test("provides templates from manifest endpoints", ({
148
- mediaEngine,
149
- expect,
150
- }) => {
151
- expect(mediaEngine.templates).toEqual({
152
- initSegment:
153
- "http://localhost:63315/api/v1/transcode/{rendition}/init.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
154
- mediaSegment:
155
- "http://localhost:63315/api/v1/transcode/{rendition}/{segmentId}.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
156
- });
157
- });
158
-
159
- test("calculatePlayheadDistance utility function", async ({ expect }) => {
160
- const { calculatePlayheadDistance } = await import(
161
- "./shared/BufferUtils.js"
162
- );
163
-
164
- // Element is currently active (playhead within element bounds)
165
- expect(
166
- calculatePlayheadDistance(
167
- { startTimeMs: 0, endTimeMs: 2000 },
168
- 1000, // playhead at 1s
169
- ),
170
- ).toBe(0);
171
-
172
- // Element hasn't started yet (playhead before element)
173
- expect(
174
- calculatePlayheadDistance(
175
- { startTimeMs: 2000, endTimeMs: 4000 },
176
- 0, // playhead at 0s
177
- ),
178
- ).toBe(2000);
179
-
180
- // Element already finished (playhead after element)
181
- expect(
182
- calculatePlayheadDistance(
183
- { startTimeMs: 0, endTimeMs: 2000 },
184
- 5000, // playhead at 5s
185
- ),
186
- ).toBe(3000);
187
-
188
- // Playhead at element start boundary
189
- expect(
190
- calculatePlayheadDistance(
191
- { startTimeMs: 2000, endTimeMs: 4000 },
192
- 2000, // playhead exactly at start
193
- ),
194
- ).toBe(0);
195
-
196
- // Playhead at element end boundary
197
- expect(
198
- calculatePlayheadDistance(
199
- { startTimeMs: 2000, endTimeMs: 4000 },
200
- 4000, // playhead exactly at end
201
- ),
202
- ).toBe(0);
203
- });
204
-
205
- test("buffer config includes timeline threshold", async ({ expect }) => {
206
- const configuration = document.createElement("ef-configuration");
207
- const apiHost = `${window.location.protocol}//${window.location.host}`;
208
- configuration.setAttribute("api-host", apiHost);
209
- configuration.apiHost = apiHost;
210
- configuration.signingURL = "";
211
-
212
- const video = document.createElement("ef-video");
213
- video.src = "http://web:3000/head-moov-480p.mp4";
214
- configuration.appendChild(video);
215
- document.body.appendChild(configuration);
216
-
217
- // Wait for media engine to initialize
218
- const mediaEngine = await video.mediaEngineTask.taskComplete;
219
-
220
- // Check that buffer config includes the threshold
221
- const bufferConfig = mediaEngine.getBufferConfig();
222
- expect(bufferConfig.bufferThresholdMs).toBe(30000);
223
-
224
- configuration.remove();
225
- });
226
- });