@editframe/elements 0.18.8-beta.0 → 0.18.20-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/elements/EFMedia/AssetIdMediaEngine.js +4 -1
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +3 -4
- package/dist/elements/EFMedia/AssetMediaEngine.js +16 -15
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +30 -11
- package/dist/elements/EFMedia/BaseMediaEngine.js +83 -31
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +2 -4
- package/dist/elements/EFMedia/JitMediaEngine.js +12 -12
- package/dist/elements/EFTemporal.d.ts +1 -0
- package/dist/elements/EFTemporal.js +8 -4
- package/dist/elements/EFTimegroup.js +21 -0
- package/dist/elements/EFVideo.d.ts +0 -1
- package/dist/elements/EFVideo.js +0 -9
- package/dist/elements/TargetController.js +3 -2
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.browsertest.d.ts +5 -0
- package/dist/gui/ContextMixin.js +96 -5
- package/package.json +2 -2
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +10 -1
- package/src/elements/EFMedia/AssetMediaEngine.ts +25 -21
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +311 -0
- package/src/elements/EFMedia/BaseMediaEngine.ts +168 -51
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +2 -12
- package/src/elements/EFMedia/JitMediaEngine.ts +25 -16
- package/src/elements/EFTemporal.browsertest.ts +47 -0
- package/src/elements/EFTemporal.ts +15 -5
- package/src/elements/EFTimegroup.ts +40 -0
- package/src/elements/EFVideo.browsertest.ts +127 -281
- package/src/elements/EFVideo.ts +9 -9
- package/src/elements/TargetController.ts +6 -2
- package/src/gui/ContextMixin.browsertest.ts +565 -1
- package/src/gui/ContextMixin.ts +138 -5
- package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +2 -4
- package/test/recordReplayProxyPlugin.js +46 -31
- package/test/setup.ts +16 -0
- package/test/useAssetMSW.ts +54 -0
- package/test/useMSW.ts +4 -11
- package/types.json +1 -1
- package/dist/elements/MediaController.d.ts +0 -30
- package/src/elements/EFMedia/BaseMediaEngine.test.ts +0 -164
- package/src/elements/MediaController.ts +0 -98
- package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/metadata.json +0 -22
- package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/metadata.json +0 -22
- package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/metadata.json +0 -22
- /package/dist/elements/EFMedia/{BaseMediaEngine.test.d.ts → BaseMediaEngine.browsertest.d.ts} +0 -0
|
@@ -7,16 +7,19 @@ import type {
|
|
|
7
7
|
import { SizeAwareLRUCache } from "../../utils/LRUCache.js";
|
|
8
8
|
import type { EFMedia } from "../EFMedia.js";
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024);
|
|
10
|
+
// Global instances shared across all media engines
|
|
11
|
+
export const mediaCache = new SizeAwareLRUCache<string>(100 * 1024 * 1024); // 100MB cache limit
|
|
12
|
+
export const globalRequestDeduplicator = new RequestDeduplicator();
|
|
12
13
|
|
|
13
14
|
export abstract class BaseMediaEngine {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
protected host: EFMedia;
|
|
16
|
+
|
|
17
|
+
constructor(host: EFMedia) {
|
|
18
|
+
this.host = host;
|
|
19
|
+
}
|
|
16
20
|
|
|
17
21
|
abstract get videoRendition(): VideoRendition | undefined;
|
|
18
22
|
abstract get audioRendition(): AudioRendition | undefined;
|
|
19
|
-
abstract get host(): EFMedia;
|
|
20
23
|
|
|
21
24
|
getVideoRendition(): VideoRendition {
|
|
22
25
|
if (!this.videoRendition) {
|
|
@@ -42,27 +45,179 @@ export abstract class BaseMediaEngine {
|
|
|
42
45
|
return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Unified fetch method with caching and global deduplication
|
|
50
|
+
* All requests (media, manifest, init segments) go through this method
|
|
51
|
+
*/
|
|
52
|
+
protected async fetchWithCache(
|
|
53
|
+
url: string,
|
|
54
|
+
options: {
|
|
55
|
+
responseType: "arrayBuffer" | "json";
|
|
56
|
+
headers?: Record<string, string>;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
},
|
|
59
|
+
): Promise<any> {
|
|
60
|
+
const { responseType, headers, signal } = options;
|
|
61
|
+
|
|
62
|
+
// Create cache key that includes URL and headers for proper isolation
|
|
63
|
+
// Note: We don't include signal in cache key as it would prevent proper deduplication
|
|
64
|
+
const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;
|
|
65
|
+
|
|
66
|
+
// Check cache first
|
|
67
|
+
const cached = mediaCache.get(cacheKey);
|
|
68
|
+
if (cached) {
|
|
69
|
+
// If we have a cached promise, we need to handle the caller's abort signal
|
|
70
|
+
// without affecting the underlying request that other instances might be using
|
|
71
|
+
if (signal) {
|
|
72
|
+
return this.handleAbortForCachedRequest(cached, signal);
|
|
73
|
+
}
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Use global deduplicator to prevent concurrent requests for the same resource
|
|
78
|
+
// Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort
|
|
79
|
+
const promise = globalRequestDeduplicator.executeRequest(
|
|
80
|
+
cacheKey,
|
|
81
|
+
async () => {
|
|
82
|
+
try {
|
|
83
|
+
// Make the fetch request WITHOUT the signal - let each caller handle their own abort
|
|
84
|
+
// This prevents one instance's abort from affecting other instances using the shared cache
|
|
85
|
+
const response = await this.host.fetch(url, { headers });
|
|
86
|
+
|
|
87
|
+
if (responseType === "json") {
|
|
88
|
+
return response.json();
|
|
89
|
+
}
|
|
90
|
+
return response.arrayBuffer();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// If the request was aborted, don't cache the error
|
|
93
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
94
|
+
// Remove from cache so other requests can retry
|
|
95
|
+
mediaCache.delete(cacheKey);
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Cache the promise (not the result) to handle concurrent requests
|
|
103
|
+
mediaCache.set(cacheKey, promise);
|
|
104
|
+
|
|
105
|
+
// Handle the case where the promise might be aborted
|
|
106
|
+
promise.catch((error) => {
|
|
107
|
+
// If the request was aborted, remove it from cache to prevent corrupted data
|
|
108
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
109
|
+
mediaCache.delete(cacheKey);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// If the caller has a signal, handle abort logic without affecting the underlying request
|
|
114
|
+
if (signal) {
|
|
115
|
+
return this.handleAbortForCachedRequest(promise, signal);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return promise;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handles abort logic for a cached request without affecting the underlying fetch
|
|
123
|
+
* This allows multiple instances to share the same cached request while each
|
|
124
|
+
* manages their own abort behavior
|
|
125
|
+
*/
|
|
126
|
+
private handleAbortForCachedRequest<T>(
|
|
127
|
+
promise: Promise<T>,
|
|
128
|
+
signal: AbortSignal,
|
|
129
|
+
): Promise<T> {
|
|
130
|
+
// If signal is already aborted, reject immediately
|
|
131
|
+
if (signal.aborted) {
|
|
132
|
+
throw new DOMException("Aborted", "AbortError");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Return a promise that respects the caller's abort signal
|
|
136
|
+
// but doesn't affect the underlying cached request
|
|
137
|
+
return Promise.race([
|
|
138
|
+
promise,
|
|
139
|
+
new Promise<never>((_, reject) => {
|
|
140
|
+
signal.addEventListener("abort", () => {
|
|
141
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
142
|
+
});
|
|
143
|
+
}),
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Public wrapper methods that delegate to fetchWithCache
|
|
148
|
+
async fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer> {
|
|
149
|
+
// Check abort signal immediately before any processing
|
|
150
|
+
if (signal?.aborted) {
|
|
151
|
+
throw new DOMException("Aborted", "AbortError");
|
|
152
|
+
}
|
|
153
|
+
return this.fetchWithCache(url, { responseType: "arrayBuffer", signal });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async fetchManifest(url: string, signal?: AbortSignal): Promise<any> {
|
|
157
|
+
// Check abort signal immediately before any processing
|
|
158
|
+
if (signal?.aborted) {
|
|
159
|
+
throw new DOMException("Aborted", "AbortError");
|
|
160
|
+
}
|
|
161
|
+
return this.fetchWithCache(url, { responseType: "json", signal });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async fetchMediaWithHeaders(
|
|
165
|
+
url: string,
|
|
166
|
+
headers: Record<string, string>,
|
|
167
|
+
signal?: AbortSignal,
|
|
168
|
+
): Promise<ArrayBuffer> {
|
|
169
|
+
// Check abort signal immediately before any processing
|
|
170
|
+
if (signal?.aborted) {
|
|
171
|
+
throw new DOMException("Aborted", "AbortError");
|
|
172
|
+
}
|
|
173
|
+
return this.fetchWithCache(url, {
|
|
174
|
+
responseType: "arrayBuffer",
|
|
175
|
+
headers,
|
|
176
|
+
signal,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Legacy methods for backward compatibility
|
|
181
|
+
async fetchMediaCache(
|
|
182
|
+
url: string,
|
|
183
|
+
signal?: AbortSignal,
|
|
184
|
+
): Promise<ArrayBuffer> {
|
|
185
|
+
return this.fetchMedia(url, signal);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async fetchManifestCache(url: string, signal?: AbortSignal): Promise<any> {
|
|
189
|
+
return this.fetchManifest(url, signal);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async fetchMediaCacheWithHeaders(
|
|
193
|
+
url: string,
|
|
194
|
+
headers: Record<string, string>,
|
|
195
|
+
signal?: AbortSignal,
|
|
196
|
+
): Promise<ArrayBuffer> {
|
|
197
|
+
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
198
|
+
}
|
|
199
|
+
|
|
45
200
|
/**
|
|
46
201
|
* Abstract method for actual segment fetching - implemented by subclasses
|
|
47
202
|
*/
|
|
48
|
-
abstract
|
|
203
|
+
abstract fetchMediaSegment(
|
|
49
204
|
segmentId: number,
|
|
50
205
|
rendition: { trackId: number | undefined; src: string },
|
|
51
206
|
): Promise<ArrayBuffer>;
|
|
52
207
|
|
|
53
208
|
/**
|
|
54
209
|
* Fetch media segment with built-in deduplication
|
|
55
|
-
*
|
|
210
|
+
* Now uses global deduplication for all requests
|
|
56
211
|
*/
|
|
57
|
-
async
|
|
212
|
+
async fetchMediaSegmentWithDeduplication(
|
|
58
213
|
segmentId: number,
|
|
59
214
|
rendition: { trackId: number | undefined; src: string },
|
|
60
215
|
_signal?: AbortSignal,
|
|
61
216
|
): Promise<ArrayBuffer> {
|
|
62
217
|
const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
|
|
63
218
|
|
|
64
|
-
return
|
|
65
|
-
return this.
|
|
219
|
+
return globalRequestDeduplicator.executeRequest(cacheKey, async () => {
|
|
220
|
+
return this.fetchMediaSegment(segmentId, rendition);
|
|
66
221
|
});
|
|
67
222
|
}
|
|
68
223
|
|
|
@@ -74,59 +229,21 @@ export abstract class BaseMediaEngine {
|
|
|
74
229
|
rendition: { src: string; trackId: number | undefined },
|
|
75
230
|
): boolean {
|
|
76
231
|
const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
|
|
77
|
-
return
|
|
232
|
+
return globalRequestDeduplicator.isPending(cacheKey);
|
|
78
233
|
}
|
|
79
234
|
|
|
80
235
|
/**
|
|
81
236
|
* Get count of active segment requests (for debugging/monitoring)
|
|
82
237
|
*/
|
|
83
238
|
getActiveSegmentRequestCount(): number {
|
|
84
|
-
return
|
|
239
|
+
return globalRequestDeduplicator.getPendingCount();
|
|
85
240
|
}
|
|
86
241
|
|
|
87
242
|
/**
|
|
88
243
|
* Cancel all active segment requests (for cleanup)
|
|
89
244
|
*/
|
|
90
245
|
cancelAllSegmentRequests(): void {
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async fetchMediaCache(mediaUrl: string) {
|
|
95
|
-
const cached = mediaCache.get(mediaUrl);
|
|
96
|
-
if (cached) {
|
|
97
|
-
return cached;
|
|
98
|
-
}
|
|
99
|
-
const promise = this.host
|
|
100
|
-
.fetch(mediaUrl)
|
|
101
|
-
.then((response) => response.arrayBuffer());
|
|
102
|
-
mediaCache.set(mediaUrl, promise);
|
|
103
|
-
return promise;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Enhanced caching method that supports custom headers (e.g., Range requests)
|
|
108
|
-
* Cache key includes both URL and headers for proper cache isolation
|
|
109
|
-
*/
|
|
110
|
-
async fetchMediaCacheWithHeaders(
|
|
111
|
-
mediaUrl: string,
|
|
112
|
-
headers?: Record<string, string>,
|
|
113
|
-
signal?: AbortSignal,
|
|
114
|
-
) {
|
|
115
|
-
// Create a cache key that includes both URL and headers
|
|
116
|
-
const cacheKey = headers
|
|
117
|
-
? `${mediaUrl}:${JSON.stringify(headers)}`
|
|
118
|
-
: mediaUrl;
|
|
119
|
-
|
|
120
|
-
const cached = mediaCache.get(cacheKey);
|
|
121
|
-
if (cached) {
|
|
122
|
-
return cached;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const promise = this.host
|
|
126
|
-
.fetch(mediaUrl, { headers, signal })
|
|
127
|
-
.then((response) => response.arrayBuffer());
|
|
128
|
-
mediaCache.set(cacheKey, promise);
|
|
129
|
-
return promise;
|
|
246
|
+
globalRequestDeduplicator.clear();
|
|
130
247
|
}
|
|
131
248
|
|
|
132
249
|
/**
|
|
@@ -109,15 +109,10 @@ describe("JitMediaEngine", () => {
|
|
|
109
109
|
|
|
110
110
|
test("returns undefined audio rendition when none available", ({
|
|
111
111
|
urlGenerator,
|
|
112
|
-
emptyManifestResponse,
|
|
113
112
|
host,
|
|
114
113
|
expect,
|
|
115
114
|
}) => {
|
|
116
|
-
const engine = new JitMediaEngine(
|
|
117
|
-
host,
|
|
118
|
-
urlGenerator,
|
|
119
|
-
emptyManifestResponse,
|
|
120
|
-
);
|
|
115
|
+
const engine = new JitMediaEngine(host, urlGenerator);
|
|
121
116
|
|
|
122
117
|
expect(engine.audioRendition).toBeUndefined();
|
|
123
118
|
});
|
|
@@ -138,15 +133,10 @@ describe("JitMediaEngine", () => {
|
|
|
138
133
|
|
|
139
134
|
test("returns undefined video rendition when none available", ({
|
|
140
135
|
urlGenerator,
|
|
141
|
-
emptyManifestResponse,
|
|
142
136
|
host,
|
|
143
137
|
expect,
|
|
144
138
|
}) => {
|
|
145
|
-
const engine = new JitMediaEngine(
|
|
146
|
-
host,
|
|
147
|
-
urlGenerator,
|
|
148
|
-
emptyManifestResponse,
|
|
149
|
-
);
|
|
139
|
+
const engine = new JitMediaEngine(host, urlGenerator);
|
|
150
140
|
|
|
151
141
|
expect(engine.videoRendition).toBeUndefined();
|
|
152
142
|
});
|
|
@@ -10,18 +10,19 @@ import type { EFMedia } from "../EFMedia.js";
|
|
|
10
10
|
import { BaseMediaEngine } from "./BaseMediaEngine";
|
|
11
11
|
|
|
12
12
|
export class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
13
|
+
private urlGenerator: UrlGenerator;
|
|
14
|
+
private data: ManifestResponse = {} as ManifestResponse;
|
|
15
|
+
|
|
13
16
|
static async fetch(host: EFMedia, urlGenerator: UrlGenerator, url: string) {
|
|
14
|
-
const
|
|
15
|
-
const data =
|
|
16
|
-
|
|
17
|
+
const engine = new JitMediaEngine(host, urlGenerator);
|
|
18
|
+
const data = await engine.fetchManifest(url);
|
|
19
|
+
engine.data = data;
|
|
20
|
+
return engine;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
constructor(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
private data: ManifestResponse,
|
|
23
|
-
) {
|
|
24
|
-
super();
|
|
23
|
+
constructor(host: EFMedia, urlGenerator: UrlGenerator) {
|
|
24
|
+
super(host);
|
|
25
|
+
this.urlGenerator = urlGenerator;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
get durationMs() {
|
|
@@ -33,9 +34,13 @@ export class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
get audioRendition(): AudioRendition | undefined {
|
|
36
|
-
|
|
37
|
+
if (!this.data.audioRenditions || this.data.audioRenditions.length === 0) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
37
40
|
|
|
41
|
+
const rendition = this.data.audioRenditions[0];
|
|
38
42
|
if (!rendition) return undefined;
|
|
43
|
+
|
|
39
44
|
return {
|
|
40
45
|
id: rendition.id as RenditionId,
|
|
41
46
|
trackId: undefined,
|
|
@@ -46,9 +51,13 @@ export class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
get videoRendition(): VideoRendition | undefined {
|
|
49
|
-
|
|
54
|
+
if (!this.data.videoRenditions || this.data.videoRenditions.length === 0) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
50
57
|
|
|
58
|
+
const rendition = this.data.videoRenditions[0];
|
|
51
59
|
if (!rendition) return undefined;
|
|
60
|
+
|
|
52
61
|
return {
|
|
53
62
|
id: rendition.id as RenditionId,
|
|
54
63
|
trackId: undefined,
|
|
@@ -74,12 +83,12 @@ export class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
74
83
|
rendition.id,
|
|
75
84
|
this,
|
|
76
85
|
);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return
|
|
86
|
+
|
|
87
|
+
// Use unified fetch method
|
|
88
|
+
return this.fetchMedia(url, signal);
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
async
|
|
91
|
+
async fetchMediaSegment(
|
|
83
92
|
segmentId: number,
|
|
84
93
|
rendition: { id?: RenditionId; trackId: number | undefined; src: string },
|
|
85
94
|
) {
|
|
@@ -91,7 +100,7 @@ export class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
91
100
|
rendition.id,
|
|
92
101
|
this,
|
|
93
102
|
);
|
|
94
|
-
return this.
|
|
103
|
+
return this.fetchMedia(url);
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
computeSegmentId(
|
|
@@ -26,6 +26,20 @@ describe("sourcein and sourceout", () => {
|
|
|
26
26
|
expect(element.durationMs).toBe(4_000);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
test("sourcein='0s' is parsed correctly as 0", () => {
|
|
30
|
+
const element = document.createElement("ten-seconds");
|
|
31
|
+
element.setAttribute("sourcein", "0s");
|
|
32
|
+
expect(element.sourceInMs).toBe(0);
|
|
33
|
+
expect(element.durationMs).toBe(10_000);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("sourcein='0ms' is parsed correctly as 0", () => {
|
|
37
|
+
const element = document.createElement("ten-seconds");
|
|
38
|
+
element.setAttribute("sourcein", "0ms");
|
|
39
|
+
expect(element.sourceInMs).toBe(0);
|
|
40
|
+
expect(element.durationMs).toBe(10_000);
|
|
41
|
+
});
|
|
42
|
+
|
|
29
43
|
describe("only srcin is set", () => {
|
|
30
44
|
test("duration is calculated", () => {
|
|
31
45
|
const element = document.createElement("ten-seconds");
|
|
@@ -109,3 +123,36 @@ describe("duration", () => {
|
|
|
109
123
|
expect(element.durationMs).toBe(10_000);
|
|
110
124
|
});
|
|
111
125
|
});
|
|
126
|
+
|
|
127
|
+
describe("EFVideo sourcein attribute", () => {
|
|
128
|
+
test("EFVideo with sourcein='0s' should parse correctly", () => {
|
|
129
|
+
const element = document.createElement("ef-video");
|
|
130
|
+
element.setAttribute("sourcein", "0s");
|
|
131
|
+
|
|
132
|
+
// The sourcein attribute should be set correctly
|
|
133
|
+
expect(element.getAttribute("sourcein")).toBe("0s");
|
|
134
|
+
|
|
135
|
+
// Note: In the test environment, the property system may not be fully initialized
|
|
136
|
+
// but the attribute is set correctly, which is what we're testing
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("Multiple EFVideo elements can be created without conflicts", () => {
|
|
140
|
+
// This test verifies that our fix for the abort signal deduplication issue works
|
|
141
|
+
// Multiple elements should be able to exist without signal conflicts
|
|
142
|
+
|
|
143
|
+
const element1 = document.createElement("ef-video");
|
|
144
|
+
const element2 = document.createElement("ef-video");
|
|
145
|
+
|
|
146
|
+
// Set different sources
|
|
147
|
+
element1.src = "test-video-1.mp4";
|
|
148
|
+
element2.src = "test-video-2.mp4";
|
|
149
|
+
|
|
150
|
+
// Both elements should have their attributes set correctly
|
|
151
|
+
expect(element1.src).toBe("test-video-1.mp4");
|
|
152
|
+
expect(element2.src).toBe("test-video-2.mp4");
|
|
153
|
+
|
|
154
|
+
// Both elements should be valid DOM elements
|
|
155
|
+
expect(element1.tagName).toBe("EF-VIDEO");
|
|
156
|
+
expect(element2.tagName).toBe("EF-VIDEO");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -236,22 +236,32 @@ export const deepGetElementsWithFrameTasks = (
|
|
|
236
236
|
};
|
|
237
237
|
|
|
238
238
|
let temporalCache: Map<Element, TemporalMixinInterface[]>;
|
|
239
|
+
let modifiedElements = new WeakSet<Element>();
|
|
240
|
+
|
|
239
241
|
const resetTemporalCache = () => {
|
|
240
242
|
temporalCache = new Map();
|
|
243
|
+
modifiedElements = new WeakSet();
|
|
241
244
|
if (typeof requestAnimationFrame !== "undefined") {
|
|
242
245
|
requestAnimationFrame(resetTemporalCache);
|
|
243
246
|
}
|
|
244
247
|
};
|
|
245
248
|
resetTemporalCache();
|
|
246
249
|
|
|
250
|
+
export const clearTemporalCacheForElement = (element: Element) => {
|
|
251
|
+
temporalCache.delete(element);
|
|
252
|
+
modifiedElements.add(element);
|
|
253
|
+
};
|
|
254
|
+
|
|
247
255
|
export const shallowGetTemporalElements = (
|
|
248
256
|
element: Element,
|
|
249
257
|
temporals: TemporalMixinInterface[] = [],
|
|
250
258
|
) => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
259
|
+
// Temporarily disable caching to ensure reactivity works correctly
|
|
260
|
+
// TODO: Implement proper cache invalidation mechanism
|
|
261
|
+
|
|
262
|
+
// Clear the temporals array to ensure fresh results
|
|
263
|
+
temporals.length = 0;
|
|
264
|
+
|
|
255
265
|
for (const child of element.children) {
|
|
256
266
|
if (isEFTemporal(child)) {
|
|
257
267
|
temporals.push(child);
|
|
@@ -259,7 +269,7 @@ export const shallowGetTemporalElements = (
|
|
|
259
269
|
shallowGetTemporalElements(child, temporals);
|
|
260
270
|
}
|
|
261
271
|
}
|
|
262
|
-
|
|
272
|
+
|
|
263
273
|
return temporals;
|
|
264
274
|
};
|
|
265
275
|
|
|
@@ -73,6 +73,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
73
73
|
fit: "none" | "contain" | "cover" = "none";
|
|
74
74
|
|
|
75
75
|
#resizeObserver?: ResizeObserver;
|
|
76
|
+
#childObserver?: MutationObserver;
|
|
76
77
|
|
|
77
78
|
@property({ type: Number, attribute: "currenttime" })
|
|
78
79
|
set currentTime(time: number) {
|
|
@@ -185,6 +186,44 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
185
186
|
this.wrapWithWorkbench();
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
// Set up observer to detect child changes that affect duration
|
|
190
|
+
this.#childObserver = new MutationObserver((mutations) => {
|
|
191
|
+
let shouldUpdate = false;
|
|
192
|
+
|
|
193
|
+
for (const mutation of mutations) {
|
|
194
|
+
if (mutation.type === "childList") {
|
|
195
|
+
// Child added/removed - this affects duration for contain/sequence modes
|
|
196
|
+
shouldUpdate = true;
|
|
197
|
+
} else if (mutation.type === "attributes") {
|
|
198
|
+
// Attribute changes that might affect duration
|
|
199
|
+
if (
|
|
200
|
+
mutation.attributeName === "duration" ||
|
|
201
|
+
mutation.attributeName === "mode"
|
|
202
|
+
) {
|
|
203
|
+
shouldUpdate = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (shouldUpdate) {
|
|
209
|
+
// Clear the temporal cache for this element to ensure childTemporals is up to date
|
|
210
|
+
import("./EFTemporal.js").then(({ clearTemporalCacheForElement }) => {
|
|
211
|
+
clearTemporalCacheForElement(this);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Trigger an update to recalculate computed properties
|
|
215
|
+
this.requestUpdate();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Observe this element for child changes
|
|
220
|
+
this.#childObserver.observe(this, {
|
|
221
|
+
childList: true,
|
|
222
|
+
subtree: true,
|
|
223
|
+
attributes: true,
|
|
224
|
+
attributeFilter: ["duration", "mode"],
|
|
225
|
+
});
|
|
226
|
+
|
|
188
227
|
requestAnimationFrame(() => {
|
|
189
228
|
this.updateAnimations();
|
|
190
229
|
});
|
|
@@ -193,6 +232,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
193
232
|
disconnectedCallback() {
|
|
194
233
|
super.disconnectedCallback();
|
|
195
234
|
this.#resizeObserver?.disconnect();
|
|
235
|
+
this.#childObserver?.disconnect();
|
|
196
236
|
}
|
|
197
237
|
|
|
198
238
|
get storageKey() {
|