@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.
- package/package.json +2 -2
- package/scripts/build-css.js +3 -3
- package/tsdown.config.ts +1 -1
- package/src/elements/ContextProxiesController.ts +0 -124
- package/src/elements/CrossUpdateController.ts +0 -22
- package/src/elements/EFAudio.browsertest.ts +0 -706
- package/src/elements/EFAudio.ts +0 -56
- package/src/elements/EFCaptions.browsertest.ts +0 -1960
- package/src/elements/EFCaptions.ts +0 -823
- package/src/elements/EFImage.browsertest.ts +0 -120
- package/src/elements/EFImage.ts +0 -113
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
- package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
- package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
- package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
- package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
- package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
- package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
- package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
- package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
- package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
- package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
- package/src/elements/EFMedia.browsertest.ts +0 -872
- package/src/elements/EFMedia.ts +0 -341
- package/src/elements/EFSourceMixin.ts +0 -60
- package/src/elements/EFSurface.browsertest.ts +0 -151
- package/src/elements/EFSurface.ts +0 -142
- package/src/elements/EFTemporal.browsertest.ts +0 -215
- package/src/elements/EFTemporal.ts +0 -800
- package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
- package/src/elements/EFThumbnailStrip.ts +0 -906
- package/src/elements/EFTimegroup.browsertest.ts +0 -934
- package/src/elements/EFTimegroup.ts +0 -882
- package/src/elements/EFVideo.browsertest.ts +0 -1482
- package/src/elements/EFVideo.ts +0 -564
- package/src/elements/EFWaveform.ts +0 -547
- package/src/elements/FetchContext.browsertest.ts +0 -401
- package/src/elements/FetchMixin.ts +0 -38
- package/src/elements/SampleBuffer.ts +0 -94
- package/src/elements/TargetController.browsertest.ts +0 -230
- package/src/elements/TargetController.ts +0 -224
- package/src/elements/TimegroupController.ts +0 -26
- package/src/elements/durationConverter.ts +0 -35
- package/src/elements/parseTimeToMs.ts +0 -9
- package/src/elements/printTaskStatus.ts +0 -16
- package/src/elements/renderTemporalAudio.ts +0 -108
- package/src/elements/updateAnimations.browsertest.ts +0 -1884
- package/src/elements/updateAnimations.ts +0 -217
- package/src/elements/util.ts +0 -24
- package/src/gui/ContextMixin.browsertest.ts +0 -860
- package/src/gui/ContextMixin.ts +0 -562
- package/src/gui/Controllable.browsertest.ts +0 -258
- package/src/gui/Controllable.ts +0 -41
- package/src/gui/EFConfiguration.ts +0 -40
- package/src/gui/EFControls.browsertest.ts +0 -389
- package/src/gui/EFControls.ts +0 -195
- package/src/gui/EFDial.browsertest.ts +0 -84
- package/src/gui/EFDial.ts +0 -172
- package/src/gui/EFFilmstrip.browsertest.ts +0 -712
- package/src/gui/EFFilmstrip.ts +0 -1349
- package/src/gui/EFFitScale.ts +0 -152
- package/src/gui/EFFocusOverlay.ts +0 -79
- package/src/gui/EFPause.browsertest.ts +0 -202
- package/src/gui/EFPause.ts +0 -73
- package/src/gui/EFPlay.browsertest.ts +0 -202
- package/src/gui/EFPlay.ts +0 -73
- package/src/gui/EFPreview.ts +0 -74
- package/src/gui/EFResizableBox.browsertest.ts +0 -79
- package/src/gui/EFResizableBox.ts +0 -898
- package/src/gui/EFScrubber.ts +0 -151
- package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
- package/src/gui/EFTimeDisplay.ts +0 -55
- package/src/gui/EFToggleLoop.ts +0 -35
- package/src/gui/EFTogglePlay.ts +0 -70
- package/src/gui/EFWorkbench.ts +0 -115
- package/src/gui/PlaybackController.ts +0 -527
- package/src/gui/TWMixin.css +0 -6
- package/src/gui/TWMixin.ts +0 -61
- package/src/gui/TargetOrContextMixin.ts +0 -185
- package/src/gui/currentTimeContext.ts +0 -5
- package/src/gui/durationContext.ts +0 -3
- package/src/gui/efContext.ts +0 -6
- package/src/gui/fetchContext.ts +0 -5
- package/src/gui/focusContext.ts +0 -7
- package/src/gui/focusedElementContext.ts +0 -5
- package/src/gui/playingContext.ts +0 -5
- package/src/otel/BridgeSpanExporter.ts +0 -150
- package/src/otel/setupBrowserTracing.ts +0 -73
- package/src/otel/tracingHelpers.ts +0 -251
- package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
- package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
- package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
- package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
- package/src/transcoding/types/index.ts +0 -312
- package/src/transcoding/utils/MediaUtils.ts +0 -63
- package/src/transcoding/utils/UrlGenerator.ts +0 -68
- package/src/transcoding/utils/constants.ts +0 -36
- package/src/utils/LRUCache.test.ts +0 -274
- 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
|
-
});
|