@editframe/elements 0.20.4-beta.0 → 0.23.6-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DelayedLoadingState.js +0 -27
- package/dist/EF_FRAMEGEN.d.ts +5 -3
- package/dist/EF_FRAMEGEN.js +49 -11
- package/dist/_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js +7 -0
- package/dist/attachContextRoot.d.ts +1 -0
- package/dist/attachContextRoot.js +9 -0
- package/dist/elements/ContextProxiesController.d.ts +1 -2
- package/dist/elements/EFAudio.js +5 -9
- package/dist/elements/EFCaptions.d.ts +1 -3
- package/dist/elements/EFCaptions.js +112 -129
- package/dist/elements/EFImage.js +6 -7
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +2 -5
- package/dist/elements/EFMedia/AssetMediaEngine.js +36 -33
- package/dist/elements/EFMedia/BaseMediaEngine.js +57 -73
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -78
- package/dist/elements/EFMedia/JitMediaEngine.js +9 -19
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +7 -13
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +2 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +6 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +9 -25
- package/dist/elements/EFMedia/shared/BufferUtils.js +2 -17
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
- package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -10
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.d.ts +29 -0
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +32 -0
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +1 -15
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +1 -7
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -5
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -70
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +11 -18
- package/dist/elements/EFMedia.d.ts +19 -0
- package/dist/elements/EFMedia.js +44 -25
- package/dist/elements/EFSourceMixin.js +5 -7
- package/dist/elements/EFSurface.js +6 -9
- package/dist/elements/EFTemporal.browsertest.d.ts +11 -0
- package/dist/elements/EFTemporal.d.ts +10 -0
- package/dist/elements/EFTemporal.js +100 -41
- package/dist/elements/EFThumbnailStrip.js +23 -73
- package/dist/elements/EFTimegroup.browsertest.d.ts +3 -3
- package/dist/elements/EFTimegroup.d.ts +35 -14
- package/dist/elements/EFTimegroup.js +138 -181
- package/dist/elements/EFVideo.d.ts +16 -2
- package/dist/elements/EFVideo.js +156 -108
- package/dist/elements/EFWaveform.js +23 -40
- package/dist/elements/SampleBuffer.js +3 -7
- package/dist/elements/TargetController.js +5 -5
- package/dist/elements/durationConverter.js +4 -4
- package/dist/elements/renderTemporalAudio.d.ts +10 -0
- package/dist/elements/renderTemporalAudio.js +35 -0
- package/dist/elements/updateAnimations.js +19 -43
- package/dist/gui/ContextMixin.d.ts +5 -5
- package/dist/gui/ContextMixin.js +167 -162
- package/dist/gui/Controllable.browsertest.d.ts +0 -0
- package/dist/gui/Controllable.d.ts +15 -0
- package/dist/gui/Controllable.js +9 -0
- package/dist/gui/EFConfiguration.js +7 -7
- package/dist/gui/EFControls.browsertest.d.ts +11 -0
- package/dist/gui/EFControls.d.ts +18 -4
- package/dist/gui/EFControls.js +70 -28
- package/dist/gui/EFDial.browsertest.d.ts +0 -0
- package/dist/gui/EFDial.d.ts +18 -0
- package/dist/gui/EFDial.js +141 -0
- package/dist/gui/EFFilmstrip.browsertest.d.ts +11 -0
- package/dist/gui/EFFilmstrip.d.ts +12 -2
- package/dist/gui/EFFilmstrip.js +214 -129
- package/dist/gui/EFFitScale.js +5 -8
- package/dist/gui/EFFocusOverlay.js +4 -4
- package/dist/gui/EFPause.browsertest.d.ts +0 -0
- package/dist/gui/EFPause.d.ts +23 -0
- package/dist/gui/EFPause.js +59 -0
- package/dist/gui/EFPlay.browsertest.d.ts +0 -0
- package/dist/gui/EFPlay.d.ts +23 -0
- package/dist/gui/EFPlay.js +59 -0
- package/dist/gui/EFPreview.d.ts +4 -0
- package/dist/gui/EFPreview.js +18 -9
- package/dist/gui/EFResizableBox.browsertest.d.ts +0 -0
- package/dist/gui/EFResizableBox.d.ts +34 -0
- package/dist/gui/EFResizableBox.js +547 -0
- package/dist/gui/EFScrubber.d.ts +9 -3
- package/dist/gui/EFScrubber.js +13 -13
- package/dist/gui/EFTimeDisplay.d.ts +7 -1
- package/dist/gui/EFTimeDisplay.js +8 -8
- package/dist/gui/EFToggleLoop.d.ts +9 -3
- package/dist/gui/EFToggleLoop.js +7 -5
- package/dist/gui/EFTogglePlay.d.ts +12 -4
- package/dist/gui/EFTogglePlay.js +26 -21
- package/dist/gui/EFWorkbench.js +5 -5
- package/dist/gui/PlaybackController.d.ts +67 -0
- package/dist/gui/PlaybackController.js +310 -0
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin2.js +1 -1
- package/dist/gui/TargetOrContextMixin.d.ts +10 -0
- package/dist/gui/TargetOrContextMixin.js +98 -0
- package/dist/gui/efContext.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -1
- package/dist/otel/BridgeSpanExporter.d.ts +13 -0
- package/dist/otel/BridgeSpanExporter.js +87 -0
- package/dist/otel/setupBrowserTracing.d.ts +12 -0
- package/dist/otel/setupBrowserTracing.js +32 -0
- package/dist/otel/tracingHelpers.d.ts +34 -0
- package/dist/otel/tracingHelpers.js +112 -0
- package/dist/style.css +1 -1
- package/dist/transcoding/cache/RequestDeduplicator.js +0 -21
- package/dist/transcoding/cache/URLTokenDeduplicator.js +1 -21
- package/dist/transcoding/utils/UrlGenerator.js +2 -19
- package/dist/utils/LRUCache.js +6 -53
- package/package.json +13 -5
- package/src/elements/ContextProxiesController.ts +10 -10
- package/src/elements/EFAudio.ts +1 -0
- package/src/elements/EFCaptions.browsertest.ts +128 -56
- package/src/elements/EFCaptions.ts +60 -34
- package/src/elements/EFImage.browsertest.ts +1 -2
- package/src/elements/EFMedia/AssetMediaEngine.ts +65 -37
- package/src/elements/EFMedia/BaseMediaEngine.ts +110 -52
- package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +3 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +7 -3
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +1 -1
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +16 -10
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -116
- package/src/elements/EFMedia.browsertest.ts +8 -15
- package/src/elements/EFMedia.ts +54 -8
- package/src/elements/EFSurface.browsertest.ts +2 -6
- package/src/elements/EFSurface.ts +1 -0
- package/src/elements/EFTemporal.browsertest.ts +58 -1
- package/src/elements/EFTemporal.ts +140 -4
- package/src/elements/EFThumbnailStrip.browsertest.ts +2 -8
- package/src/elements/EFThumbnailStrip.ts +1 -0
- package/src/elements/EFTimegroup.browsertest.ts +16 -15
- package/src/elements/EFTimegroup.ts +281 -275
- package/src/elements/EFVideo.browsertest.ts +162 -74
- package/src/elements/EFVideo.ts +229 -101
- package/src/elements/FetchContext.browsertest.ts +7 -2
- package/src/elements/TargetController.browsertest.ts +1 -0
- package/src/elements/TargetController.ts +1 -0
- package/src/elements/renderTemporalAudio.ts +108 -0
- package/src/elements/updateAnimations.browsertest.ts +181 -6
- package/src/elements/updateAnimations.ts +6 -6
- package/src/gui/ContextMixin.browsertest.ts +274 -27
- package/src/gui/ContextMixin.ts +230 -175
- package/src/gui/Controllable.browsertest.ts +258 -0
- package/src/gui/Controllable.ts +41 -0
- package/src/gui/EFControls.browsertest.ts +294 -80
- package/src/gui/EFControls.ts +139 -28
- package/src/gui/EFDial.browsertest.ts +84 -0
- package/src/gui/EFDial.ts +172 -0
- package/src/gui/EFFilmstrip.browsertest.ts +712 -0
- package/src/gui/EFFilmstrip.ts +213 -23
- package/src/gui/EFPause.browsertest.ts +202 -0
- package/src/gui/EFPause.ts +73 -0
- package/src/gui/EFPlay.browsertest.ts +202 -0
- package/src/gui/EFPlay.ts +73 -0
- package/src/gui/EFPreview.ts +20 -5
- package/src/gui/EFResizableBox.browsertest.ts +79 -0
- package/src/gui/EFResizableBox.ts +898 -0
- package/src/gui/EFScrubber.ts +7 -5
- package/src/gui/EFTimeDisplay.browsertest.ts +19 -19
- package/src/gui/EFTimeDisplay.ts +3 -1
- package/src/gui/EFToggleLoop.ts +6 -5
- package/src/gui/EFTogglePlay.ts +30 -23
- package/src/gui/PlaybackController.ts +522 -0
- package/src/gui/TWMixin.css +3 -0
- package/src/gui/TargetOrContextMixin.ts +185 -0
- package/src/gui/efContext.ts +2 -2
- package/src/otel/BridgeSpanExporter.ts +150 -0
- package/src/otel/setupBrowserTracing.ts +73 -0
- package/src/otel/tracingHelpers.ts +251 -0
- package/test/cache-integration-verification.browsertest.ts +1 -1
- package/types.json +1 -1
- package/dist/elements/ContextProxiesController.js +0 -69
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
MP4,
|
|
9
9
|
VideoSampleSink,
|
|
10
10
|
} from "mediabunny";
|
|
11
|
+
import { withSpan } from "../../otel/tracingHelpers.js";
|
|
11
12
|
import { type MediaSample, SampleBuffer } from "../SampleBuffer";
|
|
12
13
|
import { roundToMilliseconds } from "./shared/PrecisionUtils";
|
|
13
14
|
|
|
@@ -177,26 +178,39 @@ export class BufferedSeekingInput {
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
async seek(trackId: number, timeMs: number) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
}
|
|
191
203
|
|
|
192
|
-
|
|
193
|
-
|
|
204
|
+
const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);
|
|
205
|
+
this.trackSeekPromises.set(trackId, seekPromise);
|
|
194
206
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
try {
|
|
208
|
+
return await seekPromise;
|
|
209
|
+
} finally {
|
|
210
|
+
this.trackSeekPromises.delete(trackId);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
);
|
|
200
214
|
}
|
|
201
215
|
|
|
202
216
|
private async resetIterator(track: InputTrack) {
|
|
@@ -224,90 +238,193 @@ export class BufferedSeekingInput {
|
|
|
224
238
|
#seekLock?: PromiseWithResolvers<void>;
|
|
225
239
|
|
|
226
240
|
private async seekSafe(trackId: number, timeMs: number) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const firstTimestampMs = roundToMilliseconds(
|
|
239
|
-
(await track.getFirstTimestamp()) * 1000,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
if (roundedTimeMs < firstTimestampMs) {
|
|
243
|
-
console.error("Seeking outside bounds of input", {
|
|
244
|
-
roundedTimeMs,
|
|
245
|
-
firstTimestampMs,
|
|
246
|
-
});
|
|
247
|
-
throw new NoSample(
|
|
248
|
-
`Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Check if we need to reset iterator for seeks outside current buffer range
|
|
253
|
-
const bufferContents = trackBuffer.getContents();
|
|
254
|
-
if (bufferContents.length > 0) {
|
|
255
|
-
const bufferStartMs = roundToMilliseconds(
|
|
256
|
-
trackBuffer.firstTimestamp * 1000,
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
if (roundedTimeMs < bufferStartMs) {
|
|
260
|
-
await this.resetIterator(track);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const alreadyInBuffer = trackBuffer.find(timeMs);
|
|
265
|
-
if (alreadyInBuffer) return alreadyInBuffer;
|
|
266
|
-
|
|
267
|
-
const iterator = this.getTrackIterator(track);
|
|
268
|
-
while (true) {
|
|
269
|
-
const { done, value: decodedSample } = await iterator.next();
|
|
270
|
-
|
|
271
|
-
if (decodedSample) {
|
|
272
|
-
trackBuffer.push(decodedSample);
|
|
273
|
-
}
|
|
274
|
-
const foundSample = trackBuffer.find(roundedTimeMs);
|
|
275
|
-
if (foundSample) {
|
|
276
|
-
return foundSample;
|
|
277
|
-
}
|
|
278
|
-
if (done) {
|
|
279
|
-
break;
|
|
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;
|
|
280
252
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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();
|
|
300
426
|
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// For all other cases (seeking within track but outside buffer range), throw error
|
|
304
|
-
// The caller should ensure the correct segment is loaded before seeking
|
|
305
|
-
throw new NoSample(
|
|
306
|
-
`Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,
|
|
307
|
-
);
|
|
308
|
-
} finally {
|
|
309
|
-
this.#seekLock = undefined;
|
|
310
|
-
seekLock.resolve();
|
|
311
|
-
}
|
|
427
|
+
},
|
|
428
|
+
);
|
|
312
429
|
}
|
|
313
430
|
}
|
|
@@ -55,10 +55,13 @@ const test = baseTest.extend<{
|
|
|
55
55
|
const apiHost = `${window.location.protocol}//${window.location.host}`;
|
|
56
56
|
configuration.setAttribute("api-host", apiHost);
|
|
57
57
|
configuration.apiHost = apiHost;
|
|
58
|
+
configuration.signingURL = ""; // Disable URL signing for tests
|
|
58
59
|
const host = document.createElement("ef-video");
|
|
59
60
|
configuration.appendChild(host);
|
|
60
61
|
host.src = "http://web:3000/head-moov-480p.mp4";
|
|
62
|
+
document.body.appendChild(configuration);
|
|
61
63
|
await use(host);
|
|
64
|
+
configuration.remove();
|
|
62
65
|
},
|
|
63
66
|
urlGenerator: async ({}, use: any) => {
|
|
64
67
|
// UrlGenerator points to integrated proxy server (same host/port as test runner)
|
|
@@ -19,6 +19,8 @@ export const makeAudioInputTask = (host: EFMedia): InputTask => {
|
|
|
19
19
|
onComplete: (_value) => {},
|
|
20
20
|
task: async (_, { signal }) => {
|
|
21
21
|
const mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
22
|
+
if (signal.aborted) return undefined;
|
|
23
|
+
|
|
22
24
|
const audioRendition = mediaEngine?.audioRendition;
|
|
23
25
|
|
|
24
26
|
// Return undefined if no audio rendition available (video-only asset)
|
|
@@ -27,9 +29,10 @@ export const makeAudioInputTask = (host: EFMedia): InputTask => {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
const initSegment = await host.audioInitSegmentFetchTask.taskComplete;
|
|
30
|
-
signal.
|
|
32
|
+
if (signal.aborted) return undefined;
|
|
33
|
+
|
|
31
34
|
const segment = await host.audioSegmentFetchTask.taskComplete;
|
|
32
|
-
signal.
|
|
35
|
+
if (signal.aborted) return undefined;
|
|
33
36
|
|
|
34
37
|
if (!initSegment || !segment) {
|
|
35
38
|
return undefined;
|
|
@@ -38,7 +41,8 @@ export const makeAudioInputTask = (host: EFMedia): InputTask => {
|
|
|
38
41
|
const startTimeOffsetMs = audioRendition.startTimeOffsetMs;
|
|
39
42
|
|
|
40
43
|
const arrayBuffer = await new Blob([initSegment, segment]).arrayBuffer();
|
|
41
|
-
signal.
|
|
44
|
+
if (signal.aborted) return undefined;
|
|
45
|
+
|
|
42
46
|
return new BufferedSeekingInput(arrayBuffer, {
|
|
43
47
|
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
44
48
|
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts
CHANGED
|
@@ -145,7 +145,7 @@ describe("Audio Seek Task - Chunk Boundary Regression Test", () => {
|
|
|
145
145
|
|
|
146
146
|
// Now trigger the localStorage restoration that happens in waitForMediaDurations().then()
|
|
147
147
|
// This will load currentTime = 4.0 from localStorage, jumping from 0ms to 4000ms
|
|
148
|
-
const loadedTime = timegroup.
|
|
148
|
+
const loadedTime = timegroup.loadTimeFromLocalStorage();
|
|
149
149
|
if (loadedTime !== undefined) {
|
|
150
150
|
timegroup.currentTime = loadedTime;
|
|
151
151
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { BufferedSeekingInput } from "../BufferedSeekingInput";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cache for main video BufferedSeekingInput instances
|
|
5
|
+
* Main video segments are typically 2s long, so we can reuse the same input
|
|
6
|
+
* for multiple frames within that segment (e.g., 60 frames at 30fps)
|
|
7
|
+
*/
|
|
8
|
+
export class MainVideoInputCache {
|
|
9
|
+
private cache = new Map<string, BufferedSeekingInput>();
|
|
10
|
+
private maxCacheSize = 10; // Keep last 10 main inputs (covers 20 seconds at 2s/segment)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a cache key that uniquely identifies a segment
|
|
14
|
+
*/
|
|
15
|
+
private getCacheKey(
|
|
16
|
+
src: string,
|
|
17
|
+
segmentId: number,
|
|
18
|
+
renditionId: string | undefined,
|
|
19
|
+
): string {
|
|
20
|
+
return `${src}:${renditionId || "default"}:${segmentId}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get or create BufferedSeekingInput for a main video segment
|
|
25
|
+
*/
|
|
26
|
+
async getOrCreateInput(
|
|
27
|
+
src: string,
|
|
28
|
+
segmentId: number,
|
|
29
|
+
renditionId: string | undefined,
|
|
30
|
+
createInputFn: () => Promise<BufferedSeekingInput | undefined>,
|
|
31
|
+
): Promise<BufferedSeekingInput | undefined> {
|
|
32
|
+
const cacheKey = this.getCacheKey(src, segmentId, renditionId);
|
|
33
|
+
|
|
34
|
+
// Check if we already have this segment cached
|
|
35
|
+
const cached = this.cache.get(cacheKey);
|
|
36
|
+
if (cached) {
|
|
37
|
+
return cached;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create new input
|
|
41
|
+
const input = await createInputFn();
|
|
42
|
+
if (!input) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add to cache and maintain size limit
|
|
47
|
+
this.cache.set(cacheKey, input);
|
|
48
|
+
|
|
49
|
+
// Evict oldest entries if cache is too large (LRU-like behavior)
|
|
50
|
+
if (this.cache.size > this.maxCacheSize) {
|
|
51
|
+
const oldestKey = this.cache.keys().next().value;
|
|
52
|
+
if (oldestKey !== undefined) {
|
|
53
|
+
this.cache.delete(oldestKey);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return input;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Clear the entire cache (called when video changes)
|
|
62
|
+
*/
|
|
63
|
+
clear() {
|
|
64
|
+
this.cache.clear();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get cache statistics
|
|
69
|
+
*/
|
|
70
|
+
getStats() {
|
|
71
|
+
return {
|
|
72
|
+
size: this.cache.size,
|
|
73
|
+
cacheKeys: Array.from(this.cache.keys()),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -8,7 +8,7 @@ import type { InputTask } from "../shared/MediaTaskUtils";
|
|
|
8
8
|
export const makeScrubVideoInputTask = (host: EFVideo): InputTask => {
|
|
9
9
|
return new Task<
|
|
10
10
|
readonly [ArrayBuffer | undefined, ArrayBuffer | undefined],
|
|
11
|
-
BufferedSeekingInput
|
|
11
|
+
BufferedSeekingInput | undefined
|
|
12
12
|
>(host, {
|
|
13
13
|
args: () =>
|
|
14
14
|
[
|
|
@@ -19,27 +19,33 @@ export const makeScrubVideoInputTask = (host: EFVideo): InputTask => {
|
|
|
19
19
|
console.error("scrubVideoInputTask error", error);
|
|
20
20
|
},
|
|
21
21
|
onComplete: (_value) => {},
|
|
22
|
-
task: async () => {
|
|
22
|
+
task: async (_, { signal }) => {
|
|
23
23
|
const initSegment =
|
|
24
24
|
await host.scrubVideoInitSegmentFetchTask.taskComplete;
|
|
25
|
+
if (signal.aborted) return undefined;
|
|
26
|
+
|
|
25
27
|
const segment = await host.scrubVideoSegmentFetchTask.taskComplete;
|
|
28
|
+
if (signal.aborted) return undefined;
|
|
29
|
+
|
|
26
30
|
if (!initSegment || !segment) {
|
|
27
31
|
throw new Error("Scrub init segment or segment is not available");
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
// Get startTimeOffsetMs from the scrub rendition if available
|
|
31
35
|
const mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
36
|
+
if (signal.aborted) return undefined;
|
|
37
|
+
|
|
32
38
|
const scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
33
39
|
const startTimeOffsetMs = scrubRendition?.startTimeOffsetMs;
|
|
34
40
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
);
|
|
41
|
+
const arrayBuffer = await new Blob([initSegment, segment]).arrayBuffer();
|
|
42
|
+
if (signal.aborted) return undefined;
|
|
43
|
+
|
|
44
|
+
const input = new BufferedSeekingInput(arrayBuffer, {
|
|
45
|
+
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
46
|
+
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
47
|
+
startTimeOffsetMs,
|
|
48
|
+
});
|
|
43
49
|
return input;
|
|
44
50
|
},
|
|
45
51
|
});
|
|
@@ -94,13 +94,19 @@ export const makeScrubVideoSeekTask = (host: EFVideo): ScrubVideoSeekTask => {
|
|
|
94
94
|
return undefined;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
if (signal.aborted) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
97
101
|
// Get video track and seek to precise time within the 30s scrub segment
|
|
98
102
|
const videoTrack = await scrubInput.getFirstVideoTrack();
|
|
99
103
|
if (!videoTrack) {
|
|
100
104
|
return undefined;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
signal.
|
|
107
|
+
if (signal.aborted) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
104
110
|
|
|
105
111
|
const sample = (await scrubInput.seek(
|
|
106
112
|
videoTrack.id,
|