@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
|
@@ -187,6 +187,7 @@ export declare const deepGetElementsWithFrameTasks: (element: Element, elements?
|
|
|
187
187
|
}>) => (HTMLElement & {
|
|
188
188
|
frameTask: Task;
|
|
189
189
|
})[];
|
|
190
|
+
export declare const clearTemporalCacheForElement: (element: Element) => void;
|
|
190
191
|
export declare const shallowGetTemporalElements: (element: Element, temporals?: TemporalMixinInterface[]) => TemporalMixinInterface[];
|
|
191
192
|
export declare class OwnCurrentTimeController implements ReactiveController {
|
|
192
193
|
private host;
|
|
@@ -15,17 +15,21 @@ const deepGetElementsWithFrameTasks = (element, elements = []) => {
|
|
|
15
15
|
return elements;
|
|
16
16
|
};
|
|
17
17
|
let temporalCache;
|
|
18
|
+
let modifiedElements = /* @__PURE__ */ new WeakSet();
|
|
18
19
|
const resetTemporalCache = () => {
|
|
19
20
|
temporalCache = /* @__PURE__ */ new Map();
|
|
21
|
+
modifiedElements = /* @__PURE__ */ new WeakSet();
|
|
20
22
|
if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(resetTemporalCache);
|
|
21
23
|
};
|
|
22
24
|
resetTemporalCache();
|
|
25
|
+
const clearTemporalCacheForElement = (element) => {
|
|
26
|
+
temporalCache.delete(element);
|
|
27
|
+
modifiedElements.add(element);
|
|
28
|
+
};
|
|
23
29
|
const shallowGetTemporalElements = (element, temporals = []) => {
|
|
24
|
-
|
|
25
|
-
if (cachedResult) return cachedResult;
|
|
30
|
+
temporals.length = 0;
|
|
26
31
|
for (const child of element.children) if (isEFTemporal(child)) temporals.push(child);
|
|
27
32
|
else shallowGetTemporalElements(child, temporals);
|
|
28
|
-
temporalCache.set(element, temporals);
|
|
29
33
|
return temporals;
|
|
30
34
|
};
|
|
31
35
|
var OwnCurrentTimeController = class {
|
|
@@ -281,4 +285,4 @@ const EFTemporal = (superClass) => {
|
|
|
281
285
|
Object.defineProperty(TemporalMixinClass.prototype, EF_TEMPORAL, { value: true });
|
|
282
286
|
return TemporalMixinClass;
|
|
283
287
|
};
|
|
284
|
-
export { EFTemporal, deepGetElementsWithFrameTasks, flushStartTimeMsCache, isEFTemporal, shallowGetTemporalElements, timegroupContext };
|
|
288
|
+
export { EFTemporal, clearTemporalCacheForElement, deepGetElementsWithFrameTasks, flushStartTimeMsCache, isEFTemporal, shallowGetTemporalElements, timegroupContext };
|
|
@@ -53,6 +53,7 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
|
|
|
53
53
|
}
|
|
54
54
|
#currentTime = 0;
|
|
55
55
|
#resizeObserver;
|
|
56
|
+
#childObserver;
|
|
56
57
|
set currentTime(time) {
|
|
57
58
|
const newTime = Math.max(0, Math.min(time, this.durationMs / 1e3));
|
|
58
59
|
if (this.isRootTimegroup && this.isFrameUpdateInProgress) {
|
|
@@ -128,6 +129,25 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
|
|
|
128
129
|
});
|
|
129
130
|
if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
|
|
130
131
|
if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
|
|
132
|
+
this.#childObserver = new MutationObserver((mutations) => {
|
|
133
|
+
let shouldUpdate = false;
|
|
134
|
+
for (const mutation of mutations) if (mutation.type === "childList") shouldUpdate = true;
|
|
135
|
+
else if (mutation.type === "attributes") {
|
|
136
|
+
if (mutation.attributeName === "duration" || mutation.attributeName === "mode") shouldUpdate = true;
|
|
137
|
+
}
|
|
138
|
+
if (shouldUpdate) {
|
|
139
|
+
import("./EFTemporal.js").then(({ clearTemporalCacheForElement }) => {
|
|
140
|
+
clearTemporalCacheForElement(this);
|
|
141
|
+
});
|
|
142
|
+
this.requestUpdate();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
this.#childObserver.observe(this, {
|
|
146
|
+
childList: true,
|
|
147
|
+
subtree: true,
|
|
148
|
+
attributes: true,
|
|
149
|
+
attributeFilter: ["duration", "mode"]
|
|
150
|
+
});
|
|
131
151
|
requestAnimationFrame(() => {
|
|
132
152
|
this.updateAnimations();
|
|
133
153
|
});
|
|
@@ -135,6 +155,7 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
|
|
|
135
155
|
disconnectedCallback() {
|
|
136
156
|
super.disconnectedCallback();
|
|
137
157
|
this.#resizeObserver?.disconnect();
|
|
158
|
+
this.#childObserver?.disconnect();
|
|
138
159
|
}
|
|
139
160
|
get storageKey() {
|
|
140
161
|
if (!this.id) throw new Error("Timegroup must have an id to use localStorage.");
|
package/dist/getRenderInfo.d.ts
CHANGED
|
@@ -18,9 +18,9 @@ export declare const RenderInfo: z.ZodObject<{
|
|
|
18
18
|
efImage: string[];
|
|
19
19
|
}>;
|
|
20
20
|
}, "strip", z.ZodTypeAny, {
|
|
21
|
+
durationMs: number;
|
|
21
22
|
width: number;
|
|
22
23
|
height: number;
|
|
23
|
-
durationMs: number;
|
|
24
24
|
fps: number;
|
|
25
25
|
assets: {
|
|
26
26
|
efMedia: Record<string, any>;
|
|
@@ -28,9 +28,9 @@ export declare const RenderInfo: z.ZodObject<{
|
|
|
28
28
|
efImage: string[];
|
|
29
29
|
};
|
|
30
30
|
}, {
|
|
31
|
+
durationMs: number;
|
|
31
32
|
width: number;
|
|
32
33
|
height: number;
|
|
33
|
-
durationMs: number;
|
|
34
34
|
fps: number;
|
|
35
35
|
assets: {
|
|
36
36
|
efMedia: Record<string, any>;
|
|
@@ -2,9 +2,14 @@ import { LitElement } from 'lit';
|
|
|
2
2
|
declare const TestContext_base: (new (...args: any[]) => import('./ContextMixin.js').ContextMixinInterface) & typeof LitElement;
|
|
3
3
|
declare class TestContext extends TestContext_base {
|
|
4
4
|
}
|
|
5
|
+
declare const TestContextElement_base: (new (...args: any[]) => import('./ContextMixin.js').ContextMixinInterface) & typeof LitElement;
|
|
6
|
+
declare class TestContextElement extends TestContextElement_base {
|
|
7
|
+
render(): import('../elements/EFTimegroup.js').EFTimegroup & Element;
|
|
8
|
+
}
|
|
5
9
|
declare global {
|
|
6
10
|
interface HTMLElementTagNameMap {
|
|
7
11
|
"test-context": TestContext;
|
|
12
|
+
"test-context-reactivity": TestContextElement;
|
|
8
13
|
}
|
|
9
14
|
}
|
|
10
15
|
export {};
|
package/dist/gui/ContextMixin.js
CHANGED
|
@@ -24,14 +24,22 @@ function ContextMixin(superClass) {
|
|
|
24
24
|
init.headers ||= {};
|
|
25
25
|
Object.assign(init.headers, { "Content-Type": "application/json" });
|
|
26
26
|
if (this.signingURL) {
|
|
27
|
-
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);
|
|
29
|
+
const tokenExpiration = this.#URLTokenExpirations[cacheKey] || 0;
|
|
30
|
+
if (!this.#URLTokens[cacheKey] || now >= tokenExpiration) this.#URLTokens[cacheKey] = fetch(this.signingURL, {
|
|
28
31
|
method: "POST",
|
|
29
|
-
body: JSON.stringify(
|
|
32
|
+
body: JSON.stringify(signingPayload)
|
|
30
33
|
}).then(async (response) => {
|
|
31
|
-
if (response.ok)
|
|
34
|
+
if (response.ok) {
|
|
35
|
+
const tokenData = await response.json();
|
|
36
|
+
const token = tokenData.token;
|
|
37
|
+
this.#URLTokenExpirations[cacheKey] = this.#parseTokenExpiration(token);
|
|
38
|
+
return token;
|
|
39
|
+
}
|
|
32
40
|
throw new Error(`Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`);
|
|
33
41
|
});
|
|
34
|
-
const urlToken = await this.#URLTokens[
|
|
42
|
+
const urlToken = await this.#URLTokens[cacheKey];
|
|
35
43
|
Object.assign(init.headers, { authorization: `Bearer ${urlToken}` });
|
|
36
44
|
} else init.credentials = "include";
|
|
37
45
|
return fetch(url, init);
|
|
@@ -51,7 +59,76 @@ function ContextMixin(superClass) {
|
|
|
51
59
|
set apiHost(value) {
|
|
52
60
|
this.#apiHost = value;
|
|
53
61
|
}
|
|
62
|
+
get durationMs() {
|
|
63
|
+
return this.targetTimegroup?.durationMs ?? 0;
|
|
64
|
+
}
|
|
65
|
+
get endTimeMs() {
|
|
66
|
+
return this.targetTimegroup?.endTimeMs ?? 0;
|
|
67
|
+
}
|
|
54
68
|
#URLTokens = {};
|
|
69
|
+
#URLTokenExpirations = {};
|
|
70
|
+
/**
|
|
71
|
+
* Generate a cache key for URL token based on signing strategy
|
|
72
|
+
*
|
|
73
|
+
* Uses unified prefix + parameter matching approach:
|
|
74
|
+
* - For transcode URLs: signs base "/api/v1/transcode" + params like {url: "source.mp4"}
|
|
75
|
+
* - For regular URLs: signs full URL with empty params {}
|
|
76
|
+
* - All validation uses prefix matching + exhaustive parameter matching
|
|
77
|
+
* - Multiple transcode segments with same source share one token (reduces round-trips)
|
|
78
|
+
*/
|
|
79
|
+
#getTokenCacheKey(url) {
|
|
80
|
+
try {
|
|
81
|
+
const urlObj = new URL(url);
|
|
82
|
+
if (urlObj.pathname.includes("/api/v1/transcode/")) {
|
|
83
|
+
const urlParam = urlObj.searchParams.get("url");
|
|
84
|
+
if (urlParam) {
|
|
85
|
+
const basePath = `${urlObj.origin}/api/v1/transcode`;
|
|
86
|
+
const cacheKey = `${basePath}?url=${urlParam}`;
|
|
87
|
+
return {
|
|
88
|
+
cacheKey,
|
|
89
|
+
signingPayload: {
|
|
90
|
+
url: basePath,
|
|
91
|
+
params: { url: urlParam }
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
cacheKey: url,
|
|
98
|
+
signingPayload: { url }
|
|
99
|
+
};
|
|
100
|
+
} catch {
|
|
101
|
+
return {
|
|
102
|
+
cacheKey: url,
|
|
103
|
+
signingPayload: { url }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parse JWT token to extract safe expiration time (with buffer)
|
|
109
|
+
* @param token JWT token string
|
|
110
|
+
* @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails
|
|
111
|
+
*/
|
|
112
|
+
#parseTokenExpiration(token) {
|
|
113
|
+
try {
|
|
114
|
+
const parts = token.split(".");
|
|
115
|
+
if (parts.length !== 3) return 0;
|
|
116
|
+
const payload = parts[1];
|
|
117
|
+
if (!payload) return 0;
|
|
118
|
+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
119
|
+
const parsed = JSON.parse(decoded);
|
|
120
|
+
const exp = parsed.exp;
|
|
121
|
+
const iat = parsed.iat;
|
|
122
|
+
if (!exp) return 0;
|
|
123
|
+
const lifetimeSeconds = iat ? exp - iat : 3600;
|
|
124
|
+
const tenPercentBufferMs = lifetimeSeconds * .1 * 1e3;
|
|
125
|
+
const fiveMinutesMs = 5 * 60 * 1e3;
|
|
126
|
+
const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);
|
|
127
|
+
return exp * 1e3 - bufferMs;
|
|
128
|
+
} catch {
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
55
132
|
#signingURL;
|
|
56
133
|
/**
|
|
57
134
|
* A URL that will be used to generated signed tokens for accessing media files from the
|
|
@@ -66,10 +143,20 @@ function ContextMixin(superClass) {
|
|
|
66
143
|
#FPS = 30;
|
|
67
144
|
#MS_PER_FRAME = 1e3 / this.#FPS;
|
|
68
145
|
#timegroupObserver = new MutationObserver((mutations) => {
|
|
146
|
+
let shouldUpdate = false;
|
|
69
147
|
for (const mutation of mutations) if (mutation.type === "childList") {
|
|
70
148
|
const newTimegroup = this.querySelector("ef-timegroup");
|
|
71
|
-
if (newTimegroup !== this.targetTimegroup)
|
|
149
|
+
if (newTimegroup !== this.targetTimegroup) {
|
|
150
|
+
this.targetTimegroup = newTimegroup;
|
|
151
|
+
shouldUpdate = true;
|
|
152
|
+
}
|
|
153
|
+
} else if (mutation.type === "attributes") {
|
|
154
|
+
if (mutation.attributeName === "duration" || mutation.attributeName === "mode" || mutation.target instanceof Element && (mutation.target.tagName === "EF-TIMEGROUP" || mutation.target.closest("ef-timegroup"))) shouldUpdate = true;
|
|
72
155
|
}
|
|
156
|
+
if (shouldUpdate) queueMicrotask(() => {
|
|
157
|
+
this.requestUpdate();
|
|
158
|
+
if (this.targetTimegroup) this.targetTimegroup.requestUpdate();
|
|
159
|
+
});
|
|
73
160
|
});
|
|
74
161
|
connectedCallback() {
|
|
75
162
|
super.connectedCallback();
|
|
@@ -85,6 +172,8 @@ function ContextMixin(superClass) {
|
|
|
85
172
|
super.disconnectedCallback();
|
|
86
173
|
this.#timegroupObserver.disconnect();
|
|
87
174
|
this.stopPlayback();
|
|
175
|
+
this.#URLTokens = {};
|
|
176
|
+
this.#URLTokenExpirations = {};
|
|
88
177
|
}
|
|
89
178
|
update(changedProperties) {
|
|
90
179
|
if (changedProperties.has("playing")) if (this.playing) this.startPlayback();
|
|
@@ -194,6 +283,8 @@ function ContextMixin(superClass) {
|
|
|
194
283
|
})], ContextElement.prototype, "apiHost", null);
|
|
195
284
|
_decorate([provide({ context: efContext })], ContextElement.prototype, "efContext", void 0);
|
|
196
285
|
_decorate([provide({ context: targetTimegroupContext }), state()], ContextElement.prototype, "targetTimegroup", void 0);
|
|
286
|
+
_decorate([state()], ContextElement.prototype, "durationMs", null);
|
|
287
|
+
_decorate([state()], ContextElement.prototype, "endTimeMs", null);
|
|
197
288
|
_decorate([provide({ context: fetchContext })], ContextElement.prototype, "fetch", void 0);
|
|
198
289
|
_decorate([property({
|
|
199
290
|
type: String,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.20-beta.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"license": "UNLICENSED",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@bramus/style-observer": "^1.3.0",
|
|
30
|
-
"@editframe/assets": "0.18.
|
|
30
|
+
"@editframe/assets": "0.18.20-beta.0",
|
|
31
31
|
"@lit/context": "^1.1.2",
|
|
32
32
|
"@lit/task": "^1.0.1",
|
|
33
33
|
"d3": "^7.9.0",
|
|
@@ -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() {
|