@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
package/src/gui/ContextMixin.ts
CHANGED
|
@@ -71,6 +71,17 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
71
71
|
@state()
|
|
72
72
|
targetTimegroup: EFTimegroup | null = null;
|
|
73
73
|
|
|
74
|
+
// Add reactive properties that depend on the targetTimegroup
|
|
75
|
+
@state()
|
|
76
|
+
get durationMs(): number {
|
|
77
|
+
return this.targetTimegroup?.durationMs ?? 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@state()
|
|
81
|
+
get endTimeMs(): number {
|
|
82
|
+
return this.targetTimegroup?.endTimeMs ?? 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
@provide({ context: fetchContext })
|
|
75
86
|
fetch = async (url: string, init: RequestInit = {}) => {
|
|
76
87
|
init.headers ||= {};
|
|
@@ -79,13 +90,23 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
79
90
|
});
|
|
80
91
|
|
|
81
92
|
if (this.signingURL) {
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);
|
|
95
|
+
const tokenExpiration = this.#URLTokenExpirations[cacheKey] || 0;
|
|
96
|
+
|
|
97
|
+
// Check if we need to fetch a new token (no token exists or token is expired)
|
|
98
|
+
if (!this.#URLTokens[cacheKey] || now >= tokenExpiration) {
|
|
99
|
+
this.#URLTokens[cacheKey] = fetch(this.signingURL, {
|
|
84
100
|
method: "POST",
|
|
85
|
-
body: JSON.stringify(
|
|
101
|
+
body: JSON.stringify(signingPayload),
|
|
86
102
|
}).then(async (response) => {
|
|
87
103
|
if (response.ok) {
|
|
88
|
-
|
|
104
|
+
const tokenData = await response.json();
|
|
105
|
+
const token = tokenData.token;
|
|
106
|
+
// Parse and store the token's actual expiration time
|
|
107
|
+
this.#URLTokenExpirations[cacheKey] =
|
|
108
|
+
this.#parseTokenExpiration(token);
|
|
109
|
+
return token;
|
|
89
110
|
}
|
|
90
111
|
throw new Error(
|
|
91
112
|
`Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,
|
|
@@ -93,7 +114,7 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
93
114
|
});
|
|
94
115
|
}
|
|
95
116
|
|
|
96
|
-
const urlToken = await this.#URLTokens[
|
|
117
|
+
const urlToken = await this.#URLTokens[cacheKey];
|
|
97
118
|
|
|
98
119
|
Object.assign(init.headers, {
|
|
99
120
|
authorization: `Bearer ${urlToken}`,
|
|
@@ -106,6 +127,89 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
106
127
|
};
|
|
107
128
|
|
|
108
129
|
#URLTokens: Record<string, Promise<string>> = {};
|
|
130
|
+
#URLTokenExpirations: Record<string, number> = {};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generate a cache key for URL token based on signing strategy
|
|
134
|
+
*
|
|
135
|
+
* Uses unified prefix + parameter matching approach:
|
|
136
|
+
* - For transcode URLs: signs base "/api/v1/transcode" + params like {url: "source.mp4"}
|
|
137
|
+
* - For regular URLs: signs full URL with empty params {}
|
|
138
|
+
* - All validation uses prefix matching + exhaustive parameter matching
|
|
139
|
+
* - Multiple transcode segments with same source share one token (reduces round-trips)
|
|
140
|
+
*/
|
|
141
|
+
#getTokenCacheKey(url: string): {
|
|
142
|
+
cacheKey: string;
|
|
143
|
+
signingPayload: { url: string; params?: Record<string, string> };
|
|
144
|
+
} {
|
|
145
|
+
try {
|
|
146
|
+
const urlObj = new URL(url);
|
|
147
|
+
|
|
148
|
+
// Check if this is a transcode URL pattern
|
|
149
|
+
if (urlObj.pathname.includes("/api/v1/transcode/")) {
|
|
150
|
+
const urlParam = urlObj.searchParams.get("url");
|
|
151
|
+
if (urlParam) {
|
|
152
|
+
// For transcode URLs, sign the base path + url parameter
|
|
153
|
+
const basePath = `${urlObj.origin}/api/v1/transcode`;
|
|
154
|
+
const cacheKey = `${basePath}?url=${urlParam}`;
|
|
155
|
+
return {
|
|
156
|
+
cacheKey,
|
|
157
|
+
signingPayload: { url: basePath, params: { url: urlParam } },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// For non-transcode URLs, use full URL (existing behavior)
|
|
163
|
+
return {
|
|
164
|
+
cacheKey: url,
|
|
165
|
+
signingPayload: { url },
|
|
166
|
+
};
|
|
167
|
+
} catch {
|
|
168
|
+
// If URL parsing fails, fall back to full URL
|
|
169
|
+
return {
|
|
170
|
+
cacheKey: url,
|
|
171
|
+
signingPayload: { url },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse JWT token to extract safe expiration time (with buffer)
|
|
178
|
+
* @param token JWT token string
|
|
179
|
+
* @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails
|
|
180
|
+
*/
|
|
181
|
+
#parseTokenExpiration(token: string): number {
|
|
182
|
+
try {
|
|
183
|
+
// JWT has 3 parts separated by dots: header.payload.signature
|
|
184
|
+
const parts = token.split(".");
|
|
185
|
+
if (parts.length !== 3) return 0;
|
|
186
|
+
|
|
187
|
+
// Decode the payload (second part)
|
|
188
|
+
const payload = parts[1];
|
|
189
|
+
if (!payload) return 0;
|
|
190
|
+
|
|
191
|
+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
192
|
+
const parsed = JSON.parse(decoded);
|
|
193
|
+
|
|
194
|
+
// Extract timestamps (in seconds)
|
|
195
|
+
const exp = parsed.exp;
|
|
196
|
+
const iat = parsed.iat;
|
|
197
|
+
if (!exp) return 0;
|
|
198
|
+
|
|
199
|
+
// Calculate token lifetime and buffer
|
|
200
|
+
const lifetimeSeconds = iat ? exp - iat : 3600; // Default to 1 hour if no iat
|
|
201
|
+
const tenPercentBufferMs = lifetimeSeconds * 0.1 * 1000; // 10% of lifetime in ms
|
|
202
|
+
const fiveMinutesMs = 5 * 60 * 1000; // 5 minutes in ms
|
|
203
|
+
|
|
204
|
+
// Use whichever buffer is smaller (more conservative)
|
|
205
|
+
const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);
|
|
206
|
+
|
|
207
|
+
// Return expiration time minus buffer
|
|
208
|
+
return exp * 1000 - bufferMs;
|
|
209
|
+
} catch {
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
109
213
|
|
|
110
214
|
#signingURL?: string;
|
|
111
215
|
/**
|
|
@@ -138,14 +242,40 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
138
242
|
#MS_PER_FRAME = 1000 / this.#FPS;
|
|
139
243
|
|
|
140
244
|
#timegroupObserver = new MutationObserver((mutations) => {
|
|
245
|
+
let shouldUpdate = false;
|
|
246
|
+
|
|
141
247
|
for (const mutation of mutations) {
|
|
142
248
|
if (mutation.type === "childList") {
|
|
143
249
|
const newTimegroup = this.querySelector("ef-timegroup");
|
|
144
250
|
if (newTimegroup !== this.targetTimegroup) {
|
|
145
251
|
this.targetTimegroup = newTimegroup;
|
|
252
|
+
shouldUpdate = true;
|
|
253
|
+
}
|
|
254
|
+
} else if (mutation.type === "attributes") {
|
|
255
|
+
// Watch for attribute changes that might affect duration
|
|
256
|
+
if (
|
|
257
|
+
mutation.attributeName === "duration" ||
|
|
258
|
+
mutation.attributeName === "mode" ||
|
|
259
|
+
(mutation.target instanceof Element &&
|
|
260
|
+
(mutation.target.tagName === "EF-TIMEGROUP" ||
|
|
261
|
+
mutation.target.closest("ef-timegroup")))
|
|
262
|
+
) {
|
|
263
|
+
shouldUpdate = true;
|
|
146
264
|
}
|
|
147
265
|
}
|
|
148
266
|
}
|
|
267
|
+
|
|
268
|
+
if (shouldUpdate) {
|
|
269
|
+
// Trigger an update to ensure reactive properties recalculate
|
|
270
|
+
// Use a microtask to ensure DOM updates are complete
|
|
271
|
+
queueMicrotask(() => {
|
|
272
|
+
this.requestUpdate();
|
|
273
|
+
// Also ensure the targetTimegroup updates its computed properties
|
|
274
|
+
if (this.targetTimegroup) {
|
|
275
|
+
this.targetTimegroup.requestUpdate();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
149
279
|
});
|
|
150
280
|
|
|
151
281
|
connectedCallback(): void {
|
|
@@ -169,6 +299,9 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
|
|
|
169
299
|
super.disconnectedCallback();
|
|
170
300
|
this.#timegroupObserver.disconnect();
|
|
171
301
|
this.stopPlayback();
|
|
302
|
+
// Clear token cache on disconnect to prevent stale tokens
|
|
303
|
+
this.#URLTokens = {};
|
|
304
|
+
this.#URLTokenExpirations = {};
|
|
172
305
|
}
|
|
173
306
|
|
|
174
307
|
update(changedProperties: Map<string | number | symbol, unknown>) {
|
|
@@ -8,14 +8,9 @@
|
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
9
|
"content-length": "32957",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "0",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/audio/1.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
@@ -8,14 +8,9 @@
|
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
9
|
"content-length": "32502",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "0",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/audio/2.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
@@ -8,14 +8,9 @@
|
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
9
|
"content-length": "32196",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "0",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/audio/3.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
@@ -8,14 +8,9 @@
|
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
9
|
"content-length": "32570",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "0",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/audio/4.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
@@ -8,14 +8,9 @@
|
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
9
|
"content-length": "32922",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "5",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/audio/5.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
@@ -8,14 +8,9 @@
|
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
9
|
"content-length": "728",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "0",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/audio/init.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
Binary file
|
|
@@ -6,16 +6,11 @@
|
|
|
6
6
|
"access-control-allow-origin": "*",
|
|
7
7
|
"access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
|
-
"content-length": "
|
|
9
|
+
"content-length": "2057283",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "0",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/high/1.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
Binary file
|
|
@@ -6,16 +6,11 @@
|
|
|
6
6
|
"access-control-allow-origin": "*",
|
|
7
7
|
"access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
|
-
"content-length": "
|
|
9
|
+
"content-length": "2185975",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "1",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/high/2.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
Binary file
|
|
@@ -6,16 +6,11 @@
|
|
|
6
6
|
"access-control-allow-origin": "*",
|
|
7
7
|
"access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
|
-
"content-length": "
|
|
9
|
+
"content-length": "2120135",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "8",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/high/3.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
Binary file
|
|
@@ -6,16 +6,11 @@
|
|
|
6
6
|
"access-control-allow-origin": "*",
|
|
7
7
|
"access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
|
-
"content-length": "
|
|
9
|
+
"content-length": "2221511",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "0",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/high/4.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
Binary file
|
|
@@ -6,16 +6,11 @@
|
|
|
6
6
|
"access-control-allow-origin": "*",
|
|
7
7
|
"access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
|
-
"content-length": "
|
|
9
|
+
"content-length": "2037521",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "16",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/high/5.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
Binary file
|
|
@@ -6,16 +6,11 @@
|
|
|
6
6
|
"access-control-allow-origin": "*",
|
|
7
7
|
"access-control-expose-headers": "Content-Length, Content-Range, X-Cache, X-Actual-Start-Time, X-Actual-Duration, X-Transcode-Time-Ms, X-Total-Server-Time-Ms",
|
|
8
8
|
"cache-control": "public, max-age=3600",
|
|
9
|
-
"content-length": "
|
|
9
|
+
"content-length": "775",
|
|
10
10
|
"content-type": "video/iso.segment",
|
|
11
|
-
"
|
|
12
|
-
"x-cache": "HIT",
|
|
13
|
-
"x-powered-by": "Express",
|
|
14
|
-
"x-total-server-time-ms": "0",
|
|
15
|
-
"x-transcode-time-ms": "0"
|
|
11
|
+
"x-powered-by": "Express"
|
|
16
12
|
},
|
|
17
13
|
"url": "/api/v1/transcode/high/init.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
18
14
|
"method": "GET",
|
|
19
|
-
"range": null
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
"range": null
|
|
16
|
+
}
|
|
@@ -8,12 +8,10 @@
|
|
|
8
8
|
"cache-control": "public, max-age=300",
|
|
9
9
|
"content-length": "2045",
|
|
10
10
|
"content-type": "application/json; charset=utf-8",
|
|
11
|
-
"date": "Thu, 07 Aug 2025 20:55:37 GMT",
|
|
12
11
|
"etag": "W/\"81b-wi6z588RhWTgs57jivyDs3lEkkA\"",
|
|
13
12
|
"x-powered-by": "Express"
|
|
14
13
|
},
|
|
15
14
|
"url": "/api/v1/transcode/manifest.json?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
16
15
|
"method": "GET",
|
|
17
|
-
"range": null
|
|
18
|
-
|
|
19
|
-
}
|
|
16
|
+
"range": null
|
|
17
|
+
}
|
|
@@ -9,6 +9,9 @@ const CACHE_DIR = join(__dirname, "__cache__");
|
|
|
9
9
|
const TARGET_HOST = "host.docker.internal";
|
|
10
10
|
const TARGET_PORT = 3000;
|
|
11
11
|
|
|
12
|
+
// Check if we should run in cache-only mode (for CI/prepare-release)
|
|
13
|
+
const CACHE_ONLY_MODE = process.env.EF_CACHE_ONLY === "true";
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Vite plugin that adds record-and-replay proxy middleware
|
|
14
17
|
* This proxy intercepts requests to /api/v1/transcode/*, caches responses to disk,
|
|
@@ -20,7 +23,7 @@ export function recordReplayProxyPlugin() {
|
|
|
20
23
|
|
|
21
24
|
configureServer(server) {
|
|
22
25
|
console.log(
|
|
23
|
-
|
|
26
|
+
`[Proxy Plugin] Configuring record-replay proxy middleware... ${CACHE_ONLY_MODE ? "(CACHE-ONLY MODE)" : ""}`,
|
|
24
27
|
);
|
|
25
28
|
|
|
26
29
|
// Initialize cache directory
|
|
@@ -33,6 +36,11 @@ export function recordReplayProxyPlugin() {
|
|
|
33
36
|
|
|
34
37
|
console.log("[Proxy Plugin] Proxy middleware configured");
|
|
35
38
|
console.log(`[Proxy Plugin] Cache directory: ${CACHE_DIR}`);
|
|
39
|
+
if (CACHE_ONLY_MODE) {
|
|
40
|
+
console.log(
|
|
41
|
+
"[Proxy Plugin] ⚠️ Running in CACHE-ONLY mode - no remote fetching",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
36
44
|
},
|
|
37
45
|
};
|
|
38
46
|
|
|
@@ -107,6 +115,7 @@ export function recordReplayProxyPlugin() {
|
|
|
107
115
|
const headers = { ...normalized.headers };
|
|
108
116
|
delete headers.date;
|
|
109
117
|
delete headers["x-total-server-time-ms"];
|
|
118
|
+
delete headers["x-transcode-time-ms"]; // This varies between requests
|
|
110
119
|
delete headers["x-cache"]; // This can vary between HIT/MISS
|
|
111
120
|
normalized.headers = headers;
|
|
112
121
|
}
|
|
@@ -117,30 +126,6 @@ export function recordReplayProxyPlugin() {
|
|
|
117
126
|
return normalized;
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
// Check if cache should be updated by comparing normalized metadata
|
|
121
|
-
async function shouldUpdateCache(cacheDir, newMetadata) {
|
|
122
|
-
try {
|
|
123
|
-
const metadataFile = join(cacheDir, "metadata.json");
|
|
124
|
-
if (!existsSync(metadataFile)) {
|
|
125
|
-
return true; // No existing cache, should update
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const existingMetadata = JSON.parse(
|
|
129
|
-
await readFile(metadataFile, "utf-8"),
|
|
130
|
-
);
|
|
131
|
-
const normalizedExisting = normalizeMetadata(existingMetadata);
|
|
132
|
-
const normalizedNew = normalizeMetadata(newMetadata);
|
|
133
|
-
|
|
134
|
-
// Compare normalized metadata to decide if update is needed
|
|
135
|
-
return (
|
|
136
|
-
JSON.stringify(normalizedExisting) !== JSON.stringify(normalizedNew)
|
|
137
|
-
);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
console.warn(`[Proxy] Failed to check cache metadata: ${error.message}`);
|
|
140
|
-
return true; // Default to updating on error
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
129
|
// Save response to cache
|
|
145
130
|
async function cacheResponse(
|
|
146
131
|
cacheDir,
|
|
@@ -163,14 +148,15 @@ export function recordReplayProxyPlugin() {
|
|
|
163
148
|
timestamp: new Date().toISOString(),
|
|
164
149
|
};
|
|
165
150
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
151
|
+
// Always write the response to cache - binary content can change even if headers don't
|
|
152
|
+
// Write normalized metadata to disk (without dynamic fields)
|
|
153
|
+
const normalizedMetadata = normalizeMetadata(metadata);
|
|
171
154
|
|
|
172
155
|
const metadataFile = join(cacheDir, "metadata.json");
|
|
173
|
-
await writeFile(
|
|
156
|
+
await writeFile(
|
|
157
|
+
metadataFile,
|
|
158
|
+
JSON.stringify(normalizedMetadata, null, 2),
|
|
159
|
+
);
|
|
174
160
|
|
|
175
161
|
const dataFile = join(cacheDir, "data.bin");
|
|
176
162
|
await writeFile(dataFile, body); // Write raw binary data
|
|
@@ -209,6 +195,35 @@ export function recordReplayProxyPlugin() {
|
|
|
209
195
|
const cacheKey = getCacheKey(req.method, fullPath, req.headers);
|
|
210
196
|
const cacheDir = join(CACHE_DIR, cacheKey);
|
|
211
197
|
|
|
198
|
+
// In cache-only mode, try to serve from cache first
|
|
199
|
+
if (CACHE_ONLY_MODE) {
|
|
200
|
+
if (existsSync(cacheDir)) {
|
|
201
|
+
try {
|
|
202
|
+
const metadataFile = join(cacheDir, "metadata.json");
|
|
203
|
+
if (existsSync(metadataFile)) {
|
|
204
|
+
console.log(
|
|
205
|
+
`[Proxy] ✓ CACHE-ONLY: Serving from cache: ${cacheKey}`,
|
|
206
|
+
);
|
|
207
|
+
await serveCachedResponse(res, cacheDir, req);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
} catch (cacheError) {
|
|
211
|
+
console.error(`[Proxy] Failed to read cache: ${cacheError.message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(`[Proxy] ✗ CACHE-ONLY: No cache available for ${cacheKey}`);
|
|
216
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
217
|
+
res.end(
|
|
218
|
+
JSON.stringify({
|
|
219
|
+
error: "Cache-only mode enabled but no cache found",
|
|
220
|
+
cacheKey,
|
|
221
|
+
suggestion: "Run tests locally first to populate cache",
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
212
227
|
try {
|
|
213
228
|
// Collect request body
|
|
214
229
|
const requestChunks = [];
|
package/test/setup.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global test setup for all browser tests
|
|
3
|
+
* This runs before every test to ensure clean state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { beforeEach } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
globalRequestDeduplicator,
|
|
9
|
+
mediaCache,
|
|
10
|
+
} from "../src/elements/EFMedia/BaseMediaEngine.js";
|
|
11
|
+
|
|
12
|
+
// Clear global caches before each test to ensure isolation
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
globalRequestDeduplicator.clear();
|
|
15
|
+
mediaCache.clear();
|
|
16
|
+
});
|