@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,505 +0,0 @@
|
|
|
1
|
-
import { withSpan } from "../../otel/tracingHelpers.js";
|
|
2
|
-
import { RequestDeduplicator } from "../../transcoding/cache/RequestDeduplicator.js";
|
|
3
|
-
import type {
|
|
4
|
-
AudioRendition,
|
|
5
|
-
SegmentTimeRange,
|
|
6
|
-
ThumbnailResult,
|
|
7
|
-
VideoRendition,
|
|
8
|
-
} from "../../transcoding/types";
|
|
9
|
-
import { SizeAwareLRUCache } from "../../utils/LRUCache.js";
|
|
10
|
-
import type { EFMedia } from "../EFMedia.js";
|
|
11
|
-
import type { MediaRendition } from "./shared/MediaTaskUtils.js";
|
|
12
|
-
|
|
13
|
-
// Global instances shared across all media engines
|
|
14
|
-
export const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024); // 100MB cache limit
|
|
15
|
-
export const globalRequestDeduplicator = new RequestDeduplicator();
|
|
16
|
-
|
|
17
|
-
export abstract class BaseMediaEngine {
|
|
18
|
-
protected host: EFMedia;
|
|
19
|
-
|
|
20
|
-
constructor(host: EFMedia) {
|
|
21
|
-
this.host = host;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
abstract get videoRendition(): VideoRendition | undefined;
|
|
25
|
-
abstract get audioRendition(): AudioRendition | undefined;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Get video rendition if available. Returns undefined for audio-only assets.
|
|
29
|
-
* Callers should handle undefined gracefully.
|
|
30
|
-
*/
|
|
31
|
-
getVideoRendition(): VideoRendition | undefined {
|
|
32
|
-
return this.videoRendition;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Get audio rendition if available. Returns undefined for video-only assets.
|
|
37
|
-
* Callers should handle undefined gracefully.
|
|
38
|
-
*/
|
|
39
|
-
getAudioRendition(): AudioRendition | undefined {
|
|
40
|
-
return this.audioRendition;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Generate cache key for segment requests
|
|
45
|
-
*/
|
|
46
|
-
private getSegmentCacheKey(
|
|
47
|
-
segmentId: number,
|
|
48
|
-
rendition: { src: string; trackId: number | undefined; id?: string },
|
|
49
|
-
): string {
|
|
50
|
-
return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Unified fetch method with caching and global deduplication
|
|
55
|
-
* All requests (media, manifest, init segments) go through this method
|
|
56
|
-
*/
|
|
57
|
-
protected async fetchWithCache(
|
|
58
|
-
url: string,
|
|
59
|
-
options: {
|
|
60
|
-
responseType: "arrayBuffer" | "json";
|
|
61
|
-
headers?: Record<string, string>;
|
|
62
|
-
signal?: AbortSignal;
|
|
63
|
-
},
|
|
64
|
-
): Promise<any> {
|
|
65
|
-
return withSpan(
|
|
66
|
-
"mediaEngine.fetchWithCache",
|
|
67
|
-
{
|
|
68
|
-
url: url.length > 100 ? `${url.substring(0, 100)}...` : url,
|
|
69
|
-
responseType: options.responseType,
|
|
70
|
-
hasHeaders: !!options.headers,
|
|
71
|
-
},
|
|
72
|
-
undefined,
|
|
73
|
-
async (span) => {
|
|
74
|
-
const t0 = performance.now();
|
|
75
|
-
const { responseType, headers, signal } = options;
|
|
76
|
-
|
|
77
|
-
// Create cache key that includes URL and headers for proper isolation
|
|
78
|
-
// Note: We don't include signal in cache key as it would prevent proper deduplication
|
|
79
|
-
const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;
|
|
80
|
-
|
|
81
|
-
// Check cache first
|
|
82
|
-
const t1 = performance.now();
|
|
83
|
-
const cached = mediaCache.get(cacheKey);
|
|
84
|
-
const t2 = performance.now();
|
|
85
|
-
span.setAttribute("cacheLookupMs", Math.round((t2 - t1) * 1000) / 1000);
|
|
86
|
-
|
|
87
|
-
if (cached) {
|
|
88
|
-
span.setAttribute("cacheHit", true);
|
|
89
|
-
// If we have a cached promise, we need to handle the caller's abort signal
|
|
90
|
-
// without affecting the underlying request that other instances might be using
|
|
91
|
-
if (signal) {
|
|
92
|
-
const t3 = performance.now();
|
|
93
|
-
const result = await this.handleAbortForCachedRequest(
|
|
94
|
-
cached,
|
|
95
|
-
signal,
|
|
96
|
-
);
|
|
97
|
-
const t4 = performance.now();
|
|
98
|
-
span.setAttribute(
|
|
99
|
-
"handleAbortMs",
|
|
100
|
-
Math.round((t4 - t3) * 100) / 100,
|
|
101
|
-
);
|
|
102
|
-
span.setAttribute(
|
|
103
|
-
"totalCacheHitMs",
|
|
104
|
-
Math.round((t4 - t0) * 100) / 100,
|
|
105
|
-
);
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
span.setAttribute(
|
|
109
|
-
"totalCacheHitMs",
|
|
110
|
-
Math.round((t2 - t0) * 100) / 100,
|
|
111
|
-
);
|
|
112
|
-
return cached;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
span.setAttribute("cacheHit", false);
|
|
116
|
-
|
|
117
|
-
// Use global deduplicator to prevent concurrent requests for the same resource
|
|
118
|
-
// Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort
|
|
119
|
-
const promise = globalRequestDeduplicator.executeRequest(
|
|
120
|
-
cacheKey,
|
|
121
|
-
async () => {
|
|
122
|
-
const fetchStart = performance.now();
|
|
123
|
-
try {
|
|
124
|
-
// Make the fetch request WITHOUT the signal - let each caller handle their own abort
|
|
125
|
-
// This prevents one instance's abort from affecting other instances using the shared cache
|
|
126
|
-
const response = await this.host.fetch(url, { headers });
|
|
127
|
-
const fetchEnd = performance.now();
|
|
128
|
-
span.setAttribute("fetchMs", fetchEnd - fetchStart);
|
|
129
|
-
|
|
130
|
-
if (responseType === "json") {
|
|
131
|
-
return response.json();
|
|
132
|
-
}
|
|
133
|
-
const buffer = await response.arrayBuffer();
|
|
134
|
-
span.setAttribute("sizeBytes", buffer.byteLength);
|
|
135
|
-
return buffer;
|
|
136
|
-
} catch (error) {
|
|
137
|
-
// If the request was aborted, don't cache the error
|
|
138
|
-
if (
|
|
139
|
-
error instanceof DOMException &&
|
|
140
|
-
error.name === "AbortError"
|
|
141
|
-
) {
|
|
142
|
-
// Remove from cache so other requests can retry
|
|
143
|
-
mediaCache.delete(cacheKey);
|
|
144
|
-
}
|
|
145
|
-
throw error;
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
// Cache the promise (not the result) to handle concurrent requests
|
|
151
|
-
mediaCache.set(cacheKey, promise);
|
|
152
|
-
|
|
153
|
-
// Handle the case where the promise might be aborted
|
|
154
|
-
promise.catch((error) => {
|
|
155
|
-
// If the request was aborted, remove it from cache to prevent corrupted data
|
|
156
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
157
|
-
mediaCache.delete(cacheKey);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// If the caller has a signal, handle abort logic without affecting the underlying request
|
|
162
|
-
if (signal) {
|
|
163
|
-
const result = await this.handleAbortForCachedRequest(
|
|
164
|
-
promise,
|
|
165
|
-
signal,
|
|
166
|
-
);
|
|
167
|
-
const tEnd = performance.now();
|
|
168
|
-
span.setAttribute(
|
|
169
|
-
"totalFetchMs",
|
|
170
|
-
Math.round((tEnd - t0) * 100) / 100,
|
|
171
|
-
);
|
|
172
|
-
return result;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const result = await promise;
|
|
176
|
-
const tEnd = performance.now();
|
|
177
|
-
span.setAttribute("totalFetchMs", Math.round((tEnd - t0) * 100) / 100);
|
|
178
|
-
return result;
|
|
179
|
-
},
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Handles abort logic for a cached request without affecting the underlying fetch
|
|
185
|
-
* This allows multiple instances to share the same cached request while each
|
|
186
|
-
* manages their own abort behavior
|
|
187
|
-
*/
|
|
188
|
-
private handleAbortForCachedRequest<T>(
|
|
189
|
-
promise: Promise<T>,
|
|
190
|
-
signal: AbortSignal,
|
|
191
|
-
): Promise<T> {
|
|
192
|
-
// If signal is already aborted, reject immediately
|
|
193
|
-
if (signal.aborted) {
|
|
194
|
-
throw new DOMException("Aborted", "AbortError");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Return a promise that respects the caller's abort signal
|
|
198
|
-
// but doesn't affect the underlying cached request
|
|
199
|
-
return Promise.race([
|
|
200
|
-
promise,
|
|
201
|
-
new Promise<never>((_, reject) => {
|
|
202
|
-
signal.addEventListener("abort", () => {
|
|
203
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
204
|
-
});
|
|
205
|
-
}),
|
|
206
|
-
]);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Public wrapper methods that delegate to fetchWithCache
|
|
210
|
-
async fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {
|
|
211
|
-
// Check abort signal immediately before any processing
|
|
212
|
-
if (signal?.aborted) {
|
|
213
|
-
throw new DOMException("Aborted", "AbortError");
|
|
214
|
-
}
|
|
215
|
-
return this.fetchWithCache(url, { responseType: "arrayBuffer", signal });
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async fetchManifest(url: string, signal?: AbortSignal): Promise<any> {
|
|
219
|
-
// Check abort signal immediately before any processing
|
|
220
|
-
if (signal?.aborted) {
|
|
221
|
-
throw new DOMException("Aborted", "AbortError");
|
|
222
|
-
}
|
|
223
|
-
return this.fetchWithCache(url, { responseType: "json", signal });
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async fetchMediaWithHeaders(
|
|
227
|
-
url: string,
|
|
228
|
-
headers: Record<string, string>,
|
|
229
|
-
signal?: AbortSignal,
|
|
230
|
-
): Promise<ArrayBuffer> {
|
|
231
|
-
// Check abort signal immediately before any processing
|
|
232
|
-
if (signal?.aborted) {
|
|
233
|
-
throw new DOMException("Aborted", "AbortError");
|
|
234
|
-
}
|
|
235
|
-
return this.fetchWithCache(url, {
|
|
236
|
-
responseType: "arrayBuffer",
|
|
237
|
-
headers,
|
|
238
|
-
signal,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Legacy methods for backward compatibility
|
|
243
|
-
async fetchMediaCache(
|
|
244
|
-
url: string,
|
|
245
|
-
signal?: AbortSignal,
|
|
246
|
-
): Promise<ArrayBuffer> {
|
|
247
|
-
return this.fetchMedia(url, signal);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async fetchManifestCache(url: string, signal?: AbortSignal): Promise<any> {
|
|
251
|
-
return this.fetchManifest(url, signal);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async fetchMediaCacheWithHeaders(
|
|
255
|
-
url: string,
|
|
256
|
-
headers: Record<string, string>,
|
|
257
|
-
signal?: AbortSignal,
|
|
258
|
-
): Promise<ArrayBuffer> {
|
|
259
|
-
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Abstract method for actual segment fetching - implemented by subclasses
|
|
264
|
-
*/
|
|
265
|
-
abstract fetchMediaSegment(
|
|
266
|
-
segmentId: number,
|
|
267
|
-
rendition: { trackId: number | undefined; src: string },
|
|
268
|
-
): Promise<ArrayBuffer>;
|
|
269
|
-
|
|
270
|
-
abstract fetchInitSegment(
|
|
271
|
-
rendition: { trackId: number | undefined; src: string },
|
|
272
|
-
signal: AbortSignal,
|
|
273
|
-
): Promise<ArrayBuffer>;
|
|
274
|
-
|
|
275
|
-
abstract computeSegmentId(
|
|
276
|
-
desiredSeekTimeMs: number,
|
|
277
|
-
rendition: MediaRendition,
|
|
278
|
-
): number | undefined;
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Fetch media segment with built-in deduplication
|
|
282
|
-
* Now uses global deduplication for all requests
|
|
283
|
-
*/
|
|
284
|
-
async fetchMediaSegmentWithDeduplication(
|
|
285
|
-
segmentId: number,
|
|
286
|
-
rendition: { trackId: number | undefined; src: string },
|
|
287
|
-
_signal?: AbortSignal,
|
|
288
|
-
): Promise<ArrayBuffer> {
|
|
289
|
-
const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
|
|
290
|
-
|
|
291
|
-
return globalRequestDeduplicator.executeRequest(cacheKey, async () => {
|
|
292
|
-
return this.fetchMediaSegment(segmentId, rendition);
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Check if a segment is currently being fetched
|
|
298
|
-
*/
|
|
299
|
-
isSegmentBeingFetched(
|
|
300
|
-
segmentId: number,
|
|
301
|
-
rendition: { src: string; trackId: number | undefined },
|
|
302
|
-
): boolean {
|
|
303
|
-
const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
|
|
304
|
-
return globalRequestDeduplicator.isPending(cacheKey);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Get count of active segment requests (for debugging/monitoring)
|
|
309
|
-
*/
|
|
310
|
-
getActiveSegmentRequestCount(): number {
|
|
311
|
-
return globalRequestDeduplicator.getPendingCount();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Cancel all active segment requests (for cleanup)
|
|
316
|
-
*/
|
|
317
|
-
cancelAllSegmentRequests(): void {
|
|
318
|
-
globalRequestDeduplicator.clear();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Calculate audio segments needed for a time range
|
|
323
|
-
* Each media engine implements this based on their segment structure
|
|
324
|
-
*/
|
|
325
|
-
calculateAudioSegmentRange(
|
|
326
|
-
fromMs: number,
|
|
327
|
-
toMs: number,
|
|
328
|
-
rendition: AudioRendition,
|
|
329
|
-
durationMs: number,
|
|
330
|
-
): SegmentTimeRange[] {
|
|
331
|
-
// Default implementation for uniform segments (used by JitMediaEngine)
|
|
332
|
-
if (fromMs >= toMs) {
|
|
333
|
-
return [];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const segments: SegmentTimeRange[] = [];
|
|
337
|
-
|
|
338
|
-
// Use actual segment durations if available (more accurate)
|
|
339
|
-
if (
|
|
340
|
-
rendition.segmentDurationsMs &&
|
|
341
|
-
rendition.segmentDurationsMs.length > 0
|
|
342
|
-
) {
|
|
343
|
-
let cumulativeTime = 0;
|
|
344
|
-
|
|
345
|
-
for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {
|
|
346
|
-
const segmentDuration = rendition.segmentDurationsMs[i];
|
|
347
|
-
if (segmentDuration === undefined) {
|
|
348
|
-
continue; // Skip undefined segment durations
|
|
349
|
-
}
|
|
350
|
-
const segmentStartMs = cumulativeTime;
|
|
351
|
-
const segmentEndMs = Math.min(
|
|
352
|
-
cumulativeTime + segmentDuration,
|
|
353
|
-
durationMs,
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
// Don't include segments that start at or beyond the file duration
|
|
357
|
-
if (segmentStartMs >= durationMs) {
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Only include segments that overlap with requested time range
|
|
362
|
-
if (segmentStartMs < toMs && segmentEndMs > fromMs) {
|
|
363
|
-
segments.push({
|
|
364
|
-
segmentId: i + 1, // Convert to 1-based
|
|
365
|
-
startMs: segmentStartMs,
|
|
366
|
-
endMs: segmentEndMs,
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
cumulativeTime += segmentDuration;
|
|
371
|
-
|
|
372
|
-
// If we've reached or exceeded file duration, stop
|
|
373
|
-
if (cumulativeTime >= durationMs) {
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return segments;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Fall back to fixed duration calculation for backward compatibility
|
|
382
|
-
const segmentDurationMs = rendition.segmentDurationMs || 1000;
|
|
383
|
-
const startSegmentIndex = Math.floor(fromMs / segmentDurationMs);
|
|
384
|
-
const endSegmentIndex = Math.floor(toMs / segmentDurationMs);
|
|
385
|
-
|
|
386
|
-
for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {
|
|
387
|
-
const segmentId = i + 1; // Convert to 1-based
|
|
388
|
-
const segmentStartMs = i * segmentDurationMs;
|
|
389
|
-
const segmentEndMs = Math.min((i + 1) * segmentDurationMs, durationMs);
|
|
390
|
-
|
|
391
|
-
// Don't include segments that start at or beyond the file duration
|
|
392
|
-
if (segmentStartMs >= durationMs) {
|
|
393
|
-
break;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Only include segments that overlap with requested time range
|
|
397
|
-
if (segmentStartMs < toMs && segmentEndMs > fromMs) {
|
|
398
|
-
segments.push({
|
|
399
|
-
segmentId,
|
|
400
|
-
startMs: segmentStartMs,
|
|
401
|
-
endMs: segmentEndMs,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return segments;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Check if a segment is cached for a given rendition
|
|
411
|
-
* This needs to check the URL-based cache since that's where segments are actually stored
|
|
412
|
-
*/
|
|
413
|
-
isSegmentCached(
|
|
414
|
-
segmentId: number,
|
|
415
|
-
rendition: AudioRendition | VideoRendition,
|
|
416
|
-
): boolean {
|
|
417
|
-
try {
|
|
418
|
-
// Check if this is a JIT engine by looking for urlGenerator property
|
|
419
|
-
const maybeJitEngine = this as any;
|
|
420
|
-
if (
|
|
421
|
-
maybeJitEngine.urlGenerator &&
|
|
422
|
-
typeof maybeJitEngine.urlGenerator.generateSegmentUrl === "function"
|
|
423
|
-
) {
|
|
424
|
-
// This is a JIT engine - generate the URL and check URL-based cache
|
|
425
|
-
if (!rendition.id) {
|
|
426
|
-
return false;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const segmentUrl = maybeJitEngine.urlGenerator.generateSegmentUrl(
|
|
430
|
-
segmentId,
|
|
431
|
-
rendition.id,
|
|
432
|
-
maybeJitEngine,
|
|
433
|
-
);
|
|
434
|
-
const urlIsCached = mediaCache.has(segmentUrl);
|
|
435
|
-
|
|
436
|
-
return urlIsCached;
|
|
437
|
-
}
|
|
438
|
-
// For other engine types, fall back to the old segment-based key approach
|
|
439
|
-
const cacheKey = `${rendition.src}-${rendition.id || "default"}-${segmentId}-${rendition.trackId}`;
|
|
440
|
-
const isCached = mediaCache.has(cacheKey);
|
|
441
|
-
return isCached;
|
|
442
|
-
} catch (error) {
|
|
443
|
-
console.warn(
|
|
444
|
-
`🎬 BaseMediaEngine: Error checking if segment ${segmentId} is cached:`,
|
|
445
|
-
error,
|
|
446
|
-
);
|
|
447
|
-
return false;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Get cached segment IDs from a list for a given rendition
|
|
453
|
-
*/
|
|
454
|
-
getCachedSegments(
|
|
455
|
-
segmentIds: number[],
|
|
456
|
-
rendition: AudioRendition | VideoRendition,
|
|
457
|
-
): Set<number> {
|
|
458
|
-
return new Set(
|
|
459
|
-
segmentIds.filter((id) => this.isSegmentCached(id, rendition)),
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Extract thumbnail canvases at multiple timestamps efficiently
|
|
465
|
-
* Default implementation provides helpful error information
|
|
466
|
-
*/
|
|
467
|
-
async extractThumbnails(
|
|
468
|
-
timestamps: number[],
|
|
469
|
-
): Promise<(ThumbnailResult | null)[]> {
|
|
470
|
-
const engineName = this.constructor.name;
|
|
471
|
-
console.warn(
|
|
472
|
-
`${engineName}: extractThumbnails not properly implemented. ` +
|
|
473
|
-
"This MediaEngine type does not support thumbnail generation. " +
|
|
474
|
-
"Supported engines: JitMediaEngine. " +
|
|
475
|
-
`Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? "" : "s"}.`,
|
|
476
|
-
);
|
|
477
|
-
return timestamps.map(() => null);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
abstract convertToSegmentRelativeTimestamps(
|
|
481
|
-
globalTimestamps: number[],
|
|
482
|
-
segmentId: number,
|
|
483
|
-
rendition: VideoRendition,
|
|
484
|
-
): number[];
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Get buffer configuration for this media engine
|
|
488
|
-
* Can be overridden by subclasses to provide custom buffer settings
|
|
489
|
-
*/
|
|
490
|
-
getBufferConfig(): {
|
|
491
|
-
videoBufferDurationMs: number;
|
|
492
|
-
audioBufferDurationMs: number;
|
|
493
|
-
maxVideoBufferFetches: number;
|
|
494
|
-
maxAudioBufferFetches: number;
|
|
495
|
-
bufferThresholdMs: number;
|
|
496
|
-
} {
|
|
497
|
-
return {
|
|
498
|
-
videoBufferDurationMs: 10000, // 10 seconds
|
|
499
|
-
audioBufferDurationMs: 10000, // 10 seconds
|
|
500
|
-
maxVideoBufferFetches: 3,
|
|
501
|
-
maxAudioBufferFetches: 3,
|
|
502
|
-
bufferThresholdMs: 30000, // 30 seconds - timeline-aware buffering threshold
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
}
|