@editframe/elements 0.18.19-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/EFTemporal.d.ts +1 -0
- package/dist/elements/EFTemporal.js +8 -4
- package/dist/elements/EFTimegroup.js +21 -0
- 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/EFTemporal.ts +15 -5
- package/src/elements/EFTimegroup.ts +40 -0
- package/src/gui/ContextMixin.browsertest.ts +565 -1
- package/src/gui/ContextMixin.ts +138 -5
- package/types.json +1 -1
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>) {
|