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