@editframe/elements 0.26.3-beta.0 → 0.26.4-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 (132) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-css.js +3 -3
  3. package/tsdown.config.ts +1 -1
  4. package/src/elements/ContextProxiesController.ts +0 -124
  5. package/src/elements/CrossUpdateController.ts +0 -22
  6. package/src/elements/EFAudio.browsertest.ts +0 -706
  7. package/src/elements/EFAudio.ts +0 -56
  8. package/src/elements/EFCaptions.browsertest.ts +0 -1960
  9. package/src/elements/EFCaptions.ts +0 -823
  10. package/src/elements/EFImage.browsertest.ts +0 -120
  11. package/src/elements/EFImage.ts +0 -113
  12. package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
  13. package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
  14. package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
  15. package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
  16. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
  17. package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
  18. package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
  19. package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
  20. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
  21. package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
  22. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
  23. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
  24. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
  25. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
  26. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
  27. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
  28. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
  29. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
  30. package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
  31. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
  32. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
  33. package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
  34. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
  35. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
  36. package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
  37. package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
  38. package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
  39. package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
  40. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
  41. package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
  42. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
  43. package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
  44. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
  45. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
  46. package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
  47. package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
  48. package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
  49. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
  50. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
  51. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
  52. package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
  53. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
  54. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
  55. package/src/elements/EFMedia.browsertest.ts +0 -872
  56. package/src/elements/EFMedia.ts +0 -341
  57. package/src/elements/EFSourceMixin.ts +0 -60
  58. package/src/elements/EFSurface.browsertest.ts +0 -151
  59. package/src/elements/EFSurface.ts +0 -142
  60. package/src/elements/EFTemporal.browsertest.ts +0 -215
  61. package/src/elements/EFTemporal.ts +0 -800
  62. package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
  63. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
  64. package/src/elements/EFThumbnailStrip.ts +0 -906
  65. package/src/elements/EFTimegroup.browsertest.ts +0 -934
  66. package/src/elements/EFTimegroup.ts +0 -882
  67. package/src/elements/EFVideo.browsertest.ts +0 -1482
  68. package/src/elements/EFVideo.ts +0 -564
  69. package/src/elements/EFWaveform.ts +0 -547
  70. package/src/elements/FetchContext.browsertest.ts +0 -401
  71. package/src/elements/FetchMixin.ts +0 -38
  72. package/src/elements/SampleBuffer.ts +0 -94
  73. package/src/elements/TargetController.browsertest.ts +0 -230
  74. package/src/elements/TargetController.ts +0 -224
  75. package/src/elements/TimegroupController.ts +0 -26
  76. package/src/elements/durationConverter.ts +0 -35
  77. package/src/elements/parseTimeToMs.ts +0 -9
  78. package/src/elements/printTaskStatus.ts +0 -16
  79. package/src/elements/renderTemporalAudio.ts +0 -108
  80. package/src/elements/updateAnimations.browsertest.ts +0 -1884
  81. package/src/elements/updateAnimations.ts +0 -217
  82. package/src/elements/util.ts +0 -24
  83. package/src/gui/ContextMixin.browsertest.ts +0 -860
  84. package/src/gui/ContextMixin.ts +0 -562
  85. package/src/gui/Controllable.browsertest.ts +0 -258
  86. package/src/gui/Controllable.ts +0 -41
  87. package/src/gui/EFConfiguration.ts +0 -40
  88. package/src/gui/EFControls.browsertest.ts +0 -389
  89. package/src/gui/EFControls.ts +0 -195
  90. package/src/gui/EFDial.browsertest.ts +0 -84
  91. package/src/gui/EFDial.ts +0 -172
  92. package/src/gui/EFFilmstrip.browsertest.ts +0 -712
  93. package/src/gui/EFFilmstrip.ts +0 -1349
  94. package/src/gui/EFFitScale.ts +0 -152
  95. package/src/gui/EFFocusOverlay.ts +0 -79
  96. package/src/gui/EFPause.browsertest.ts +0 -202
  97. package/src/gui/EFPause.ts +0 -73
  98. package/src/gui/EFPlay.browsertest.ts +0 -202
  99. package/src/gui/EFPlay.ts +0 -73
  100. package/src/gui/EFPreview.ts +0 -74
  101. package/src/gui/EFResizableBox.browsertest.ts +0 -79
  102. package/src/gui/EFResizableBox.ts +0 -898
  103. package/src/gui/EFScrubber.ts +0 -151
  104. package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
  105. package/src/gui/EFTimeDisplay.ts +0 -55
  106. package/src/gui/EFToggleLoop.ts +0 -35
  107. package/src/gui/EFTogglePlay.ts +0 -70
  108. package/src/gui/EFWorkbench.ts +0 -115
  109. package/src/gui/PlaybackController.ts +0 -527
  110. package/src/gui/TWMixin.css +0 -6
  111. package/src/gui/TWMixin.ts +0 -61
  112. package/src/gui/TargetOrContextMixin.ts +0 -185
  113. package/src/gui/currentTimeContext.ts +0 -5
  114. package/src/gui/durationContext.ts +0 -3
  115. package/src/gui/efContext.ts +0 -6
  116. package/src/gui/fetchContext.ts +0 -5
  117. package/src/gui/focusContext.ts +0 -7
  118. package/src/gui/focusedElementContext.ts +0 -5
  119. package/src/gui/playingContext.ts +0 -5
  120. package/src/otel/BridgeSpanExporter.ts +0 -150
  121. package/src/otel/setupBrowserTracing.ts +0 -73
  122. package/src/otel/tracingHelpers.ts +0 -251
  123. package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
  124. package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
  125. package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
  126. package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
  127. package/src/transcoding/types/index.ts +0 -312
  128. package/src/transcoding/utils/MediaUtils.ts +0 -63
  129. package/src/transcoding/utils/UrlGenerator.ts +0 -68
  130. package/src/transcoding/utils/constants.ts +0 -36
  131. package/src/utils/LRUCache.test.ts +0 -274
  132. 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
- });