@editframe/elements 0.19.4-beta.0 → 0.20.0-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/ContextProxiesController.d.ts +40 -0
- package/dist/elements/ContextProxiesController.js +69 -0
- package/dist/elements/EFCaptions.d.ts +45 -6
- package/dist/elements/EFCaptions.js +220 -26
- package/dist/elements/EFImage.js +4 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +2 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +9 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +11 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +13 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +9 -0
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +7 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +24 -0
- package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
- package/dist/elements/EFMedia.js +25 -1
- package/dist/elements/EFSurface.browsertest.d.ts +0 -0
- package/dist/elements/EFSurface.d.ts +30 -0
- package/dist/elements/EFSurface.js +96 -0
- package/dist/elements/EFTemporal.js +7 -6
- package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
- package/dist/elements/EFThumbnailStrip.d.ts +86 -0
- package/dist/elements/EFThumbnailStrip.js +490 -0
- package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
- package/dist/elements/EFTimegroup.d.ts +6 -1
- package/dist/elements/EFTimegroup.js +46 -10
- package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
- package/dist/elements/updateAnimations.d.ts +5 -0
- package/dist/elements/updateAnimations.js +37 -13
- package/dist/getRenderInfo.js +1 -1
- package/dist/gui/ContextMixin.js +27 -14
- package/dist/gui/EFControls.browsertest.d.ts +0 -0
- package/dist/gui/EFControls.d.ts +38 -0
- package/dist/gui/EFControls.js +51 -0
- package/dist/gui/EFFilmstrip.d.ts +40 -1
- package/dist/gui/EFFilmstrip.js +240 -3
- package/dist/gui/EFPreview.js +2 -1
- package/dist/gui/EFScrubber.d.ts +6 -5
- package/dist/gui/EFScrubber.js +31 -21
- package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
- package/dist/gui/EFTimeDisplay.d.ts +2 -6
- package/dist/gui/EFTimeDisplay.js +13 -23
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/currentTimeContext.d.ts +3 -0
- package/dist/gui/currentTimeContext.js +3 -0
- package/dist/gui/durationContext.d.ts +3 -0
- package/dist/gui/durationContext.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/dist/style.css +1 -1
- package/dist/transcoding/types/index.d.ts +11 -0
- package/dist/utils/LRUCache.d.ts +46 -0
- package/dist/utils/LRUCache.js +382 -1
- package/dist/utils/LRUCache.test.d.ts +1 -0
- package/package.json +2 -2
- package/src/elements/ContextProxiesController.ts +123 -0
- package/src/elements/EFCaptions.browsertest.ts +1820 -0
- package/src/elements/EFCaptions.ts +373 -36
- package/src/elements/EFImage.ts +4 -1
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
- package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
- package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +48 -0
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
- package/src/elements/EFMedia.ts +38 -1
- package/src/elements/EFSurface.browsertest.ts +155 -0
- package/src/elements/EFSurface.ts +141 -0
- package/src/elements/EFTemporal.ts +14 -8
- package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
- package/src/elements/EFThumbnailStrip.ts +905 -0
- package/src/elements/EFTimegroup.browsertest.ts +56 -7
- package/src/elements/EFTimegroup.ts +70 -11
- package/src/elements/updateAnimations.browsertest.ts +333 -11
- package/src/elements/updateAnimations.ts +68 -19
- package/src/gui/ContextMixin.browsertest.ts +0 -25
- package/src/gui/ContextMixin.ts +44 -20
- package/src/gui/EFControls.browsertest.ts +175 -0
- package/src/gui/EFControls.ts +84 -0
- package/src/gui/EFFilmstrip.ts +323 -4
- package/src/gui/EFPreview.ts +2 -1
- package/src/gui/EFScrubber.ts +29 -25
- package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
- package/src/gui/EFTimeDisplay.ts +12 -40
- package/src/gui/currentTimeContext.ts +5 -0
- package/src/gui/durationContext.ts +3 -0
- package/src/transcoding/types/index.ts +13 -0
- package/src/utils/LRUCache.test.ts +272 -0
- package/src/utils/LRUCache.ts +543 -0
- package/types.json +1 -1
- package/dist/transcoding/cache/CacheManager.d.ts +0 -73
- package/src/transcoding/cache/CacheManager.ts +0 -208
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { OrderedLRUCache } from "../utils/LRUCache.js";
|
|
2
|
+
import { TargetController } from "./TargetController.js";
|
|
3
|
+
import { Task } from "@lit/task";
|
|
4
|
+
import { LitElement, css, html } from "lit";
|
|
5
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
6
|
+
import _decorate from "@oxc-project/runtime/helpers/decorate";
|
|
7
|
+
import { createRef, ref } from "lit/directives/ref.js";
|
|
8
|
+
/**
|
|
9
|
+
* Global thumbnail image cache for smooth resize performance
|
|
10
|
+
* Shared across all thumbnail strip instances
|
|
11
|
+
* Uses OrderedLRUCache for efficient timestamp-based searching
|
|
12
|
+
*/
|
|
13
|
+
const thumbnailImageCache = new OrderedLRUCache(200, (a, b) => {
|
|
14
|
+
const partsA = a.split(":");
|
|
15
|
+
const partsB = b.split(":");
|
|
16
|
+
const timeA = Number.parseFloat(partsA[partsA.length - 1] || "0");
|
|
17
|
+
const timeB = Number.parseFloat(partsB[partsB.length - 1] || "0");
|
|
18
|
+
return timeA - timeB;
|
|
19
|
+
});
|
|
20
|
+
globalThis.debugThumbnailCache = thumbnailImageCache;
|
|
21
|
+
/**
|
|
22
|
+
* Quantize timestamp to 30fps frame boundaries for consistent caching
|
|
23
|
+
* This eliminates cache misses from floating point precision differences
|
|
24
|
+
*/
|
|
25
|
+
function quantizeTimestamp(timeMs) {
|
|
26
|
+
const frameIntervalMs = 1e3 / 30;
|
|
27
|
+
return Math.round(timeMs / frameIntervalMs) * frameIntervalMs;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate cache key for thumbnail image data (dimension-independent, quantized)
|
|
31
|
+
*/
|
|
32
|
+
function getThumbnailCacheKey(videoSrc, timeMs) {
|
|
33
|
+
const quantizedTimeMs = quantizeTimestamp(timeMs);
|
|
34
|
+
return `${videoSrc}:${quantizedTimeMs}`;
|
|
35
|
+
}
|
|
36
|
+
const THUMBNAIL_GAP = 1;
|
|
37
|
+
const STRIP_BORDER_PADDING = 4;
|
|
38
|
+
/**
|
|
39
|
+
* Calculate optimal thumbnail count and timestamps for the strip
|
|
40
|
+
* Groups thumbnails by scrub segment ID for efficient caching
|
|
41
|
+
*/
|
|
42
|
+
function calculateThumbnailLayout(stripWidth, thumbnailWidth, startTimeMs, endTimeMs, scrubSegmentDurationMs) {
|
|
43
|
+
if (stripWidth <= 0 || thumbnailWidth <= 0 || endTimeMs <= startTimeMs) return {
|
|
44
|
+
count: 0,
|
|
45
|
+
segments: []
|
|
46
|
+
};
|
|
47
|
+
const thumbnailPitch = thumbnailWidth + THUMBNAIL_GAP;
|
|
48
|
+
const baseFitCount = Math.floor(stripWidth / thumbnailPitch);
|
|
49
|
+
const count = Math.max(1, baseFitCount + 1);
|
|
50
|
+
const timestamps = [];
|
|
51
|
+
const timeRange = endTimeMs - startTimeMs;
|
|
52
|
+
for (let i = 0; i < count; i++) {
|
|
53
|
+
const timeMs = count === 1 ? (startTimeMs + endTimeMs) / 2 : startTimeMs + i * timeRange / (count - 1);
|
|
54
|
+
timestamps.push(timeMs);
|
|
55
|
+
}
|
|
56
|
+
const segmentMap = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const timeMs of timestamps) {
|
|
58
|
+
const segmentId = scrubSegmentDurationMs ? Math.floor(timeMs / scrubSegmentDurationMs) : 0;
|
|
59
|
+
if (!segmentMap.has(segmentId)) segmentMap.set(segmentId, []);
|
|
60
|
+
segmentMap.get(segmentId).push({ timeMs });
|
|
61
|
+
}
|
|
62
|
+
const segments = Array.from(segmentMap.entries()).sort(([a], [b]) => a - b).map(([segmentId, thumbnails]) => ({
|
|
63
|
+
segmentId,
|
|
64
|
+
thumbnails
|
|
65
|
+
}));
|
|
66
|
+
return {
|
|
67
|
+
count,
|
|
68
|
+
segments
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
72
|
+
constructor(..._args) {
|
|
73
|
+
super(..._args);
|
|
74
|
+
this.canvasRef = createRef();
|
|
75
|
+
this._targetController = new TargetController(this);
|
|
76
|
+
this._targetElement = null;
|
|
77
|
+
this.target = "";
|
|
78
|
+
this.thumbnailWidth = 80;
|
|
79
|
+
this.useIntrinsicDuration = false;
|
|
80
|
+
this._stripWidth = 0;
|
|
81
|
+
this._stripHeight = 48;
|
|
82
|
+
this._thumbnailUpdateInProgress = false;
|
|
83
|
+
this._pendingThumbnailUpdate = false;
|
|
84
|
+
this.thumbnailLayoutTask = new Task(this, {
|
|
85
|
+
autoRun: false,
|
|
86
|
+
task: async ([stripWidth, thumbnailWidth, targetElement, startTimeMs, endTimeMs, useIntrinsicDuration, mediaEngine]) => {
|
|
87
|
+
if (stripWidth <= 0 || !targetElement) return {
|
|
88
|
+
count: 0,
|
|
89
|
+
segments: []
|
|
90
|
+
};
|
|
91
|
+
if (!mediaEngine) {
|
|
92
|
+
if (targetElement.mediaEngineTask) {
|
|
93
|
+
await targetElement.mediaEngineTask.taskComplete;
|
|
94
|
+
const readyMediaEngine = targetElement.mediaEngineTask.value;
|
|
95
|
+
if (!readyMediaEngine) return {
|
|
96
|
+
count: 0,
|
|
97
|
+
segments: []
|
|
98
|
+
};
|
|
99
|
+
return this.calculateLayoutWithMediaEngine(stripWidth, thumbnailWidth, targetElement, startTimeMs, endTimeMs, useIntrinsicDuration, readyMediaEngine);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
count: 0,
|
|
103
|
+
segments: []
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return this.calculateLayoutWithMediaEngine(stripWidth, thumbnailWidth, targetElement, startTimeMs, endTimeMs, useIntrinsicDuration, mediaEngine);
|
|
107
|
+
},
|
|
108
|
+
args: () => [
|
|
109
|
+
this.stripWidth,
|
|
110
|
+
this.thumbnailWidth,
|
|
111
|
+
this.targetElement,
|
|
112
|
+
this.startTimeMs,
|
|
113
|
+
this.endTimeMs,
|
|
114
|
+
this.useIntrinsicDuration,
|
|
115
|
+
this.targetElement?.mediaEngineTask?.value
|
|
116
|
+
]
|
|
117
|
+
});
|
|
118
|
+
this.thumbnailRenderTask = new Task(this, {
|
|
119
|
+
autoRun: false,
|
|
120
|
+
task: async ([layout, targetElement, thumbnailWidth]) => {
|
|
121
|
+
if (!layout || !targetElement) return [];
|
|
122
|
+
return this.renderThumbnails(layout, targetElement, thumbnailWidth);
|
|
123
|
+
},
|
|
124
|
+
args: () => [
|
|
125
|
+
this.thumbnailLayoutTask.value || null,
|
|
126
|
+
this.targetElement,
|
|
127
|
+
this.thumbnailWidth
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
static {
|
|
132
|
+
this.styles = [css`
|
|
133
|
+
:host {
|
|
134
|
+
display: block;
|
|
135
|
+
position: relative;
|
|
136
|
+
width: 100%;
|
|
137
|
+
height: 48px; /* Default filmstrip height */
|
|
138
|
+
background: #2a2a2a;
|
|
139
|
+
border: 2px solid #333;
|
|
140
|
+
border-radius: 6px;
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
143
|
+
}
|
|
144
|
+
canvas {
|
|
145
|
+
display: block;
|
|
146
|
+
position: absolute;
|
|
147
|
+
top: 0;
|
|
148
|
+
left: 0;
|
|
149
|
+
image-rendering: pixelated; /* Keep thumbnails crisp */
|
|
150
|
+
/* Width and height set programmatically to prevent CSS scaling */
|
|
151
|
+
}
|
|
152
|
+
.loading-overlay {
|
|
153
|
+
position: absolute;
|
|
154
|
+
top: 0;
|
|
155
|
+
left: 0;
|
|
156
|
+
right: 0;
|
|
157
|
+
bottom: 0;
|
|
158
|
+
background: rgba(42, 42, 42, 0.9);
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
justify-content: center;
|
|
162
|
+
font-size: 11px;
|
|
163
|
+
color: #ccc;
|
|
164
|
+
font-weight: 500;
|
|
165
|
+
}
|
|
166
|
+
`];
|
|
167
|
+
}
|
|
168
|
+
get targetElement() {
|
|
169
|
+
return this._targetElement;
|
|
170
|
+
}
|
|
171
|
+
set targetElement(value) {
|
|
172
|
+
const oldValue = this._targetElement;
|
|
173
|
+
this._targetElement = value;
|
|
174
|
+
this._videoPropertyObserver?.disconnect();
|
|
175
|
+
if (value && value !== oldValue) {
|
|
176
|
+
this._videoPropertyObserver = new MutationObserver((mutations) => {
|
|
177
|
+
let shouldUpdate = false;
|
|
178
|
+
for (const mutation of mutations) if (mutation.type === "attributes" && mutation.attributeName) {
|
|
179
|
+
const attr = mutation.attributeName;
|
|
180
|
+
if (attr === "trimstart" || attr === "trimend" || attr === "sourcein" || attr === "sourceout" || attr === "src") {
|
|
181
|
+
shouldUpdate = true;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (shouldUpdate) this.runThumbnailUpdate();
|
|
186
|
+
});
|
|
187
|
+
this._videoPropertyObserver.observe(value, {
|
|
188
|
+
attributes: true,
|
|
189
|
+
attributeFilter: [
|
|
190
|
+
"trimstart",
|
|
191
|
+
"trimend",
|
|
192
|
+
"sourcein",
|
|
193
|
+
"sourceout",
|
|
194
|
+
"src"
|
|
195
|
+
]
|
|
196
|
+
});
|
|
197
|
+
if (value.mediaEngineTask) value.mediaEngineTask.taskComplete.then(() => {
|
|
198
|
+
if (this._stripWidth > 0) this.thumbnailLayoutTask.run();
|
|
199
|
+
}).catch(() => {});
|
|
200
|
+
}
|
|
201
|
+
this.requestUpdate("targetElement", oldValue);
|
|
202
|
+
}
|
|
203
|
+
set stripWidth(value) {
|
|
204
|
+
if (this._thumbnailLayoutTask) {
|
|
205
|
+
this._pendingStripWidth = value;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
this._stripWidth = value;
|
|
209
|
+
if (value > 0) this._thumbnailLayoutTask = this.thumbnailLayoutTask.run().then(async () => {
|
|
210
|
+
await this.thumbnailLayoutTask.taskComplete;
|
|
211
|
+
const layout = this.thumbnailLayoutTask.value;
|
|
212
|
+
return layout ? this.runThumbnailRenderTask(layout) : [];
|
|
213
|
+
}).finally(() => {
|
|
214
|
+
this._thumbnailLayoutTask = void 0;
|
|
215
|
+
if (this._pendingStripWidth) {
|
|
216
|
+
this.stripWidth = this._pendingStripWidth;
|
|
217
|
+
this._pendingStripWidth = void 0;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
get stripWidth() {
|
|
222
|
+
return this._stripWidth;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Run thumbnail render task directly with provided layout (bypasses task args dependency)
|
|
226
|
+
*/
|
|
227
|
+
async runThumbnailRenderTask(layout) {
|
|
228
|
+
if (!layout || !this.targetElement || layout.count === 0) return [];
|
|
229
|
+
return this.renderThumbnails(layout, this.targetElement, this.thumbnailWidth);
|
|
230
|
+
}
|
|
231
|
+
updated(changedProperties) {
|
|
232
|
+
super.updated(changedProperties);
|
|
233
|
+
if (this._stripWidth === 0) {
|
|
234
|
+
const width = this.clientWidth;
|
|
235
|
+
if (width > 0) this.stripWidth = width;
|
|
236
|
+
}
|
|
237
|
+
if (changedProperties.has("thumbnailWidth") || changedProperties.has("startTimeMs") || changedProperties.has("endTimeMs") || changedProperties.has("useIntrinsicDuration")) this.runThumbnailUpdate();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Run thumbnail update with responsive debouncing (based on EFTimegroup currentTime pattern)
|
|
241
|
+
*/
|
|
242
|
+
runThumbnailUpdate() {
|
|
243
|
+
if (this._thumbnailUpdateInProgress) {
|
|
244
|
+
this._pendingThumbnailUpdate = true;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this._thumbnailUpdateInProgress = true;
|
|
248
|
+
this.thumbnailLayoutTask.run().then(async () => {
|
|
249
|
+
await this.thumbnailLayoutTask.taskComplete;
|
|
250
|
+
const layout = this.thumbnailLayoutTask.value;
|
|
251
|
+
if (layout) await this.runThumbnailRenderTask(layout);
|
|
252
|
+
}).catch(() => {}).finally(() => {
|
|
253
|
+
this._thumbnailUpdateInProgress = false;
|
|
254
|
+
if (this._pendingThumbnailUpdate) {
|
|
255
|
+
this._pendingThumbnailUpdate = false;
|
|
256
|
+
this.runThumbnailUpdate();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Calculate layout with a ready media engine
|
|
262
|
+
*/
|
|
263
|
+
calculateLayoutWithMediaEngine(stripWidth, thumbnailWidth, targetElement, startTimeMs, endTimeMs, useIntrinsicDuration, mediaEngine) {
|
|
264
|
+
if (useIntrinsicDuration) {
|
|
265
|
+
const effectiveStartMs$1 = startTimeMs ?? 0;
|
|
266
|
+
const effectiveEndMs$1 = endTimeMs ?? targetElement.intrinsicDurationMs ?? 0;
|
|
267
|
+
return this.generateLayoutFromTimeRange(stripWidth, thumbnailWidth, effectiveStartMs$1, effectiveEndMs$1, mediaEngine);
|
|
268
|
+
}
|
|
269
|
+
const sourceStart = targetElement.sourceStartMs ?? 0;
|
|
270
|
+
const trimmedDuration = targetElement.durationMs ?? 0;
|
|
271
|
+
const effectiveStartMs = startTimeMs !== void 0 ? sourceStart + startTimeMs : sourceStart;
|
|
272
|
+
const effectiveEndMs = endTimeMs !== void 0 ? sourceStart + endTimeMs : sourceStart + trimmedDuration;
|
|
273
|
+
return this.generateLayoutFromTimeRange(stripWidth, thumbnailWidth, effectiveStartMs, effectiveEndMs, mediaEngine);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Generate layout from calculated time range
|
|
277
|
+
*/
|
|
278
|
+
generateLayoutFromTimeRange(stripWidth, thumbnailWidth, effectiveStartMs, effectiveEndMs, mediaEngine) {
|
|
279
|
+
const scrubSegmentDurationMs = mediaEngine && typeof mediaEngine.getScrubVideoRendition === "function" ? mediaEngine.getScrubVideoRendition()?.segmentDurationMs : void 0;
|
|
280
|
+
const layout = calculateThumbnailLayout(stripWidth, thumbnailWidth, effectiveStartMs, effectiveEndMs, scrubSegmentDurationMs);
|
|
281
|
+
return layout;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Render thumbnails with provided layout (main rendering logic)
|
|
285
|
+
*/
|
|
286
|
+
async renderThumbnails(layout, targetElement, thumbnailWidth) {
|
|
287
|
+
if (!layout || !targetElement || layout.count === 0) return [];
|
|
288
|
+
const videoSrc = targetElement.src;
|
|
289
|
+
const availableHeight = this._stripHeight - STRIP_BORDER_PADDING;
|
|
290
|
+
const allThumbnails = [];
|
|
291
|
+
let thumbnailIndex = 0;
|
|
292
|
+
for (const segment of layout.segments) for (const thumbnail of segment.thumbnails) {
|
|
293
|
+
const cacheKey = getThumbnailCacheKey(videoSrc, thumbnail.timeMs);
|
|
294
|
+
let imageData = thumbnailImageCache.get(cacheKey);
|
|
295
|
+
let status = "exact-hit";
|
|
296
|
+
let nearHitKey;
|
|
297
|
+
if (!imageData) {
|
|
298
|
+
const timeMinus = Math.max(0, thumbnail.timeMs - 5e3);
|
|
299
|
+
const timePlus = thumbnail.timeMs + 5e3;
|
|
300
|
+
const rangeStartKey = `${videoSrc}:${timeMinus}`;
|
|
301
|
+
const rangeEndKey = `${videoSrc}:${timePlus}`;
|
|
302
|
+
const nearHits = thumbnailImageCache.findRange(rangeStartKey, rangeEndKey);
|
|
303
|
+
const sameVideoHits = nearHits.filter((hit) => hit.key.startsWith(`${videoSrc}:`));
|
|
304
|
+
if (sameVideoHits.length > 0) {
|
|
305
|
+
const nearestHit = sameVideoHits.reduce((closest, current) => {
|
|
306
|
+
const currentParts = current.key.split(":");
|
|
307
|
+
const closestParts = closest.key.split(":");
|
|
308
|
+
const currentTime = Number.parseFloat(currentParts[currentParts.length - 1] || "0");
|
|
309
|
+
const closestTime = Number.parseFloat(closestParts[closestParts.length - 1] || "0");
|
|
310
|
+
const currentDiff = Math.abs(currentTime - thumbnail.timeMs);
|
|
311
|
+
const closestDiff = Math.abs(closestTime - thumbnail.timeMs);
|
|
312
|
+
return currentDiff < closestDiff ? current : closest;
|
|
313
|
+
});
|
|
314
|
+
imageData = nearestHit.value;
|
|
315
|
+
status = "near-hit";
|
|
316
|
+
nearHitKey = nearestHit.key;
|
|
317
|
+
} else status = "missing";
|
|
318
|
+
}
|
|
319
|
+
const x = thumbnailIndex * (thumbnailWidth + THUMBNAIL_GAP);
|
|
320
|
+
allThumbnails.push({
|
|
321
|
+
timeMs: thumbnail.timeMs,
|
|
322
|
+
segmentId: segment.segmentId,
|
|
323
|
+
x,
|
|
324
|
+
width: thumbnailWidth,
|
|
325
|
+
height: availableHeight,
|
|
326
|
+
status,
|
|
327
|
+
imageData,
|
|
328
|
+
nearHitKey
|
|
329
|
+
});
|
|
330
|
+
thumbnailIndex++;
|
|
331
|
+
}
|
|
332
|
+
await this.drawThumbnails(allThumbnails);
|
|
333
|
+
await this.loadMissingThumbnails(allThumbnails, targetElement);
|
|
334
|
+
return allThumbnails;
|
|
335
|
+
}
|
|
336
|
+
connectedCallback() {
|
|
337
|
+
super.connectedCallback();
|
|
338
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
339
|
+
for (const entry of entries) {
|
|
340
|
+
const width = entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0]?.inlineSize : entry.contentRect.width;
|
|
341
|
+
const height = entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0]?.blockSize : entry.contentRect.height;
|
|
342
|
+
this._stripHeight = height ?? 0;
|
|
343
|
+
this.stripWidth = width ?? 0;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
this.resizeObserver.observe(this);
|
|
347
|
+
this.updateComplete.then(() => {
|
|
348
|
+
if (this._stripWidth === 0) {
|
|
349
|
+
const width = this.clientWidth;
|
|
350
|
+
if (width > 0) this.stripWidth = width ?? 0;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
disconnectedCallback() {
|
|
355
|
+
super.disconnectedCallback();
|
|
356
|
+
this.resizeObserver?.disconnect();
|
|
357
|
+
this.resizeObserver = void 0;
|
|
358
|
+
this._videoPropertyObserver?.disconnect();
|
|
359
|
+
this._videoPropertyObserver = void 0;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Draw thumbnails to the canvas with cache hits and placeholders
|
|
363
|
+
*/
|
|
364
|
+
async drawThumbnails(thumbnails) {
|
|
365
|
+
const canvas = this.canvasRef.value;
|
|
366
|
+
if (!canvas) return;
|
|
367
|
+
const ctx = canvas.getContext("2d");
|
|
368
|
+
if (!ctx) return;
|
|
369
|
+
const dpr = window.devicePixelRatio || 1;
|
|
370
|
+
canvas.width = this._stripWidth * dpr;
|
|
371
|
+
canvas.height = this._stripHeight * dpr;
|
|
372
|
+
canvas.style.width = `${this._stripWidth}px`;
|
|
373
|
+
canvas.style.height = `${this._stripHeight}px`;
|
|
374
|
+
ctx.scale(dpr, dpr);
|
|
375
|
+
ctx.fillStyle = "#2a2a2a";
|
|
376
|
+
ctx.fillRect(0, 0, this._stripWidth, this._stripHeight);
|
|
377
|
+
for (const thumb of thumbnails) if (thumb.imageData) {
|
|
378
|
+
const tempCanvas = document.createElement("canvas");
|
|
379
|
+
tempCanvas.width = thumb.imageData.width;
|
|
380
|
+
tempCanvas.height = thumb.imageData.height;
|
|
381
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
382
|
+
if (!tempCtx) continue;
|
|
383
|
+
tempCtx.putImageData(thumb.imageData, 0, 0);
|
|
384
|
+
const sourceAspect = thumb.imageData.width / thumb.imageData.height;
|
|
385
|
+
const containerAspect = thumb.width / thumb.height;
|
|
386
|
+
let drawWidth;
|
|
387
|
+
let drawHeight;
|
|
388
|
+
let drawX;
|
|
389
|
+
let drawY;
|
|
390
|
+
if (sourceAspect > containerAspect) {
|
|
391
|
+
drawWidth = thumb.width;
|
|
392
|
+
drawHeight = Math.round(thumb.width / sourceAspect);
|
|
393
|
+
drawX = thumb.x;
|
|
394
|
+
drawY = Math.round((this._stripHeight - drawHeight) / 2);
|
|
395
|
+
} else {
|
|
396
|
+
drawWidth = Math.round(thumb.height * sourceAspect);
|
|
397
|
+
drawHeight = thumb.height;
|
|
398
|
+
drawX = thumb.x + Math.round((thumb.width - drawWidth) / 2);
|
|
399
|
+
drawY = Math.round((this._stripHeight - drawHeight) / 2);
|
|
400
|
+
}
|
|
401
|
+
ctx.drawImage(tempCanvas, drawX, drawY, drawWidth, drawHeight);
|
|
402
|
+
if (thumb.status === "near-hit") {
|
|
403
|
+
ctx.fillStyle = "rgba(255, 165, 0, 0.3)";
|
|
404
|
+
ctx.fillRect(thumb.x, 0, thumb.width, 2);
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
const placeholderY = Math.round((this._stripHeight - thumb.height) / 2);
|
|
408
|
+
ctx.fillStyle = "#404040";
|
|
409
|
+
ctx.fillRect(thumb.x, placeholderY, thumb.width, thumb.height);
|
|
410
|
+
ctx.strokeStyle = "#666";
|
|
411
|
+
ctx.lineWidth = 1;
|
|
412
|
+
ctx.setLineDash([2, 2]);
|
|
413
|
+
ctx.strokeRect(thumb.x, placeholderY, thumb.width, thumb.height);
|
|
414
|
+
ctx.setLineDash([]);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Load missing thumbnails using MediaEngine batch extraction
|
|
419
|
+
*/
|
|
420
|
+
async loadMissingThumbnails(thumbnails, targetElement) {
|
|
421
|
+
const mediaEngine = targetElement.mediaEngineTask?.value;
|
|
422
|
+
if (!mediaEngine) return;
|
|
423
|
+
const missingThumbnails = thumbnails.filter((t) => t.status === "missing" || t.status === "near-hit");
|
|
424
|
+
if (missingThumbnails.length === 0) return;
|
|
425
|
+
for (const thumb of missingThumbnails) thumb.status = "loading";
|
|
426
|
+
const timestamps = missingThumbnails.map((t) => t.timeMs);
|
|
427
|
+
const thumbnailResults = await mediaEngine.extractThumbnails(timestamps);
|
|
428
|
+
for (let i = 0; i < missingThumbnails.length; i++) {
|
|
429
|
+
const thumb = missingThumbnails[i];
|
|
430
|
+
const thumbnailResult = thumbnailResults[i];
|
|
431
|
+
if (thumb && thumbnailResult) {
|
|
432
|
+
const imageData = this.canvasToImageData(thumbnailResult.thumbnail);
|
|
433
|
+
if (imageData) {
|
|
434
|
+
const cacheKey = getThumbnailCacheKey(targetElement.src, thumb.timeMs);
|
|
435
|
+
thumbnailImageCache.set(cacheKey, imageData);
|
|
436
|
+
thumb.imageData = imageData;
|
|
437
|
+
thumb.status = "exact-hit";
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
await this.drawThumbnails(thumbnails);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Convert Canvas to ImageData for caching
|
|
445
|
+
*/
|
|
446
|
+
canvasToImageData(canvas) {
|
|
447
|
+
const ctx = canvas.getContext("2d");
|
|
448
|
+
if (!ctx) return null;
|
|
449
|
+
return ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
450
|
+
}
|
|
451
|
+
render() {
|
|
452
|
+
return html`
|
|
453
|
+
<canvas ${ref(this.canvasRef)}></canvas>
|
|
454
|
+
${this.thumbnailRenderTask.render({
|
|
455
|
+
pending: () => html``,
|
|
456
|
+
complete: () => html``,
|
|
457
|
+
error: (e) => html`<div class="error">Error loading thumbnails: ${e}</div>`
|
|
458
|
+
})}
|
|
459
|
+
`;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
_decorate([state()], EFThumbnailStrip.prototype, "targetElement", null);
|
|
463
|
+
_decorate([property({ type: String })], EFThumbnailStrip.prototype, "target", void 0);
|
|
464
|
+
_decorate([property({
|
|
465
|
+
type: Number,
|
|
466
|
+
attribute: "thumbnail-width"
|
|
467
|
+
})], EFThumbnailStrip.prototype, "thumbnailWidth", void 0);
|
|
468
|
+
_decorate([property({
|
|
469
|
+
type: Number,
|
|
470
|
+
attribute: "start-time-ms"
|
|
471
|
+
})], EFThumbnailStrip.prototype, "startTimeMs", void 0);
|
|
472
|
+
_decorate([property({
|
|
473
|
+
type: Number,
|
|
474
|
+
attribute: "end-time-ms"
|
|
475
|
+
})], EFThumbnailStrip.prototype, "endTimeMs", void 0);
|
|
476
|
+
_decorate([property({
|
|
477
|
+
type: Boolean,
|
|
478
|
+
attribute: "use-intrinsic-duration",
|
|
479
|
+
reflect: true,
|
|
480
|
+
converter: {
|
|
481
|
+
fromAttribute: (value) => {
|
|
482
|
+
if (value === null) return false;
|
|
483
|
+
return value === "true";
|
|
484
|
+
},
|
|
485
|
+
toAttribute: (value) => value ? "true" : null
|
|
486
|
+
}
|
|
487
|
+
})], EFThumbnailStrip.prototype, "useIntrinsicDuration", void 0);
|
|
488
|
+
_decorate([state()], EFThumbnailStrip.prototype, "stripWidth", null);
|
|
489
|
+
EFThumbnailStrip = _decorate([customElement("ef-thumbnail-strip")], EFThumbnailStrip);
|
|
490
|
+
export { EFThumbnailStrip };
|
|
File without changes
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Task } from '@lit/task';
|
|
2
|
-
import { LitElement } from 'lit';
|
|
2
|
+
import { LitElement, PropertyValues } from 'lit';
|
|
3
3
|
export declare const flushSequenceDurationCache: () => void;
|
|
4
4
|
export declare const shallowGetTimegroups: (element: Element, groups?: EFTimegroup[]) => EFTimegroup[];
|
|
5
5
|
declare const EFTimegroup_base: (new (...args: any[]) => import('./EFTemporal.js').TemporalMixinInterface) & typeof LitElement;
|
|
@@ -14,6 +14,10 @@ export declare class EFTimegroup extends EFTimegroup_base {
|
|
|
14
14
|
get overlapMs(): number;
|
|
15
15
|
private _overlapMs;
|
|
16
16
|
fit: "none" | "contain" | "cover";
|
|
17
|
+
/**
|
|
18
|
+
* Throttles frameTask execution to ensure only one runs at a time while preserving the last request
|
|
19
|
+
*/
|
|
20
|
+
private runThrottledFrameTask;
|
|
17
21
|
set currentTime(time: number);
|
|
18
22
|
get currentTime(): number;
|
|
19
23
|
set currentTimeMs(ms: number);
|
|
@@ -25,6 +29,7 @@ export declare class EFTimegroup extends EFTimegroup_base {
|
|
|
25
29
|
render(): import('lit-html').TemplateResult<1>;
|
|
26
30
|
maybeLoadTimeFromLocalStorage(): number | undefined;
|
|
27
31
|
connectedCallback(): void;
|
|
32
|
+
protected updated(_changedProperties: PropertyValues): void;
|
|
28
33
|
disconnectedCallback(): void;
|
|
29
34
|
get storageKey(): string;
|
|
30
35
|
get intrinsicDurationMs(): number | undefined;
|
|
@@ -4,7 +4,7 @@ import { durationConverter } from "./durationConverter.js";
|
|
|
4
4
|
import { EFTemporal, deepGetElementsWithFrameTasks, flushStartTimeMsCache, resetTemporalCache, shallowGetTemporalElements, timegroupContext } from "./EFTemporal.js";
|
|
5
5
|
import { deepGetMediaElements } from "./EFMedia.js";
|
|
6
6
|
import { TimegroupController } from "./TimegroupController.js";
|
|
7
|
-
import {
|
|
7
|
+
import { evaluateTemporalStateForAnimation, updateAnimations } from "./updateAnimations.js";
|
|
8
8
|
import { provide } from "@lit/context";
|
|
9
9
|
import { Task, TaskStatus } from "@lit/task";
|
|
10
10
|
import debug from "debug";
|
|
@@ -51,9 +51,10 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
|
|
|
51
51
|
if (!this.isRootTimegroup) return;
|
|
52
52
|
await this.waitForMediaDurations();
|
|
53
53
|
const newTime = Math.max(0, Math.min(targetTime ?? 0, this.durationMs / 1e3));
|
|
54
|
+
this.#currentTime = newTime;
|
|
54
55
|
this.requestUpdate("currentTime");
|
|
55
|
-
await this.
|
|
56
|
-
this.#saveTimeToLocalStorage(
|
|
56
|
+
await this.runThrottledFrameTask();
|
|
57
|
+
this.#saveTimeToLocalStorage(this.#currentTime);
|
|
57
58
|
this.#seekInProgress = false;
|
|
58
59
|
return newTime;
|
|
59
60
|
}
|
|
@@ -90,6 +91,34 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
|
|
|
90
91
|
#seekInProgress = false;
|
|
91
92
|
#pendingSeekTime;
|
|
92
93
|
#processingPendingSeek = false;
|
|
94
|
+
#frameTaskInProgress = false;
|
|
95
|
+
#pendingFrameTaskRun = false;
|
|
96
|
+
#processingPendingFrameTask = false;
|
|
97
|
+
/**
|
|
98
|
+
* Throttles frameTask execution to ensure only one runs at a time while preserving the last request
|
|
99
|
+
*/
|
|
100
|
+
async runThrottledFrameTask() {
|
|
101
|
+
if (this.#frameTaskInProgress) {
|
|
102
|
+
this.#pendingFrameTaskRun = true;
|
|
103
|
+
while (this.#frameTaskInProgress) await this.frameTask.taskComplete;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.#frameTaskInProgress = true;
|
|
107
|
+
try {
|
|
108
|
+
await this.frameTask.run();
|
|
109
|
+
} finally {
|
|
110
|
+
this.#frameTaskInProgress = false;
|
|
111
|
+
if (this.#pendingFrameTaskRun && !this.#processingPendingFrameTask) {
|
|
112
|
+
this.#pendingFrameTaskRun = false;
|
|
113
|
+
this.#processingPendingFrameTask = true;
|
|
114
|
+
try {
|
|
115
|
+
await this.runThrottledFrameTask();
|
|
116
|
+
} finally {
|
|
117
|
+
this.#processingPendingFrameTask = false;
|
|
118
|
+
}
|
|
119
|
+
} else this.#pendingFrameTaskRun = false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
93
122
|
set currentTime(time) {
|
|
94
123
|
time = Math.max(0, time);
|
|
95
124
|
if (!this.isRootTimegroup) return;
|
|
@@ -171,6 +200,13 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
|
|
|
171
200
|
if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
|
|
172
201
|
if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
|
|
173
202
|
}
|
|
203
|
+
#previousDurationMs = 0;
|
|
204
|
+
updated(_changedProperties) {
|
|
205
|
+
if (this.#previousDurationMs !== this.durationMs) {
|
|
206
|
+
this.#previousDurationMs = this.durationMs;
|
|
207
|
+
this.runThrottledFrameTask();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
174
210
|
disconnectedCallback() {
|
|
175
211
|
super.disconnectedCallback();
|
|
176
212
|
this.#resizeObserver?.disconnect();
|
|
@@ -228,7 +264,9 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
|
|
|
228
264
|
const startTimeMs = temporal.startTimeMs;
|
|
229
265
|
const endTimeMs = temporal.endTimeMs;
|
|
230
266
|
const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
|
|
231
|
-
const
|
|
267
|
+
const isRootTimegroup = temporal.tagName.toLowerCase() === "ef-timegroup" && !temporal.parentTimegroup;
|
|
268
|
+
const useInclusiveEnd = isRootTimegroup;
|
|
269
|
+
const elementEndsAfterStart = useInclusiveEnd ? endTimeMs >= timelineTimeMs : endTimeMs > timelineTimeMs;
|
|
232
270
|
return elementStartsBeforeEnd && elementEndsAfterStart;
|
|
233
271
|
});
|
|
234
272
|
const frameTasks = activeTemporals.map((temporal) => temporal.frameTask);
|
|
@@ -252,12 +290,10 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
|
|
|
252
290
|
async waitForFrameTasks() {
|
|
253
291
|
const temporalElements = deepGetElementsWithFrameTasks(this);
|
|
254
292
|
const visibleElements = temporalElements.filter((element) => {
|
|
255
|
-
const
|
|
256
|
-
return
|
|
293
|
+
const animationState = evaluateTemporalStateForAnimation(element);
|
|
294
|
+
return animationState.isVisible;
|
|
257
295
|
});
|
|
258
|
-
await Promise.all(visibleElements.map((element) =>
|
|
259
|
-
return element.frameTask.run();
|
|
260
|
-
}));
|
|
296
|
+
await Promise.all(visibleElements.map((element) => element.frameTask.run()));
|
|
261
297
|
}
|
|
262
298
|
async waitForMediaDurations() {
|
|
263
299
|
if (!this.mediaDurationsPromise) this.mediaDurationsPromise = this.#waitForMediaDurations();
|
|
@@ -415,4 +451,4 @@ _decorate([property({
|
|
|
415
451
|
attribute: "currenttime"
|
|
416
452
|
})], EFTimegroup.prototype, "currentTime", null);
|
|
417
453
|
EFTimegroup = _EFTimegroup = _decorate([customElement("ef-timegroup")], EFTimegroup);
|
|
418
|
-
export { EFTimegroup, shallowGetTimegroups };
|
|
454
|
+
export { EFTimegroup, flushSequenceDurationCache, shallowGetTimegroups };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
declare const TestTemporalElement_base: (new (...args: any[]) => import('./EFTemporal.js').TemporalMixinInterface) & typeof LitElement;
|
|
3
|
+
declare class TestTemporalElement extends TestTemporalElement_base {
|
|
4
|
+
get intrinsicDurationMs(): number;
|
|
5
|
+
private _durationMs;
|
|
6
|
+
setDuration(duration: number): void;
|
|
7
|
+
}
|
|
8
|
+
declare global {
|
|
9
|
+
interface HTMLElementTagNameMap {
|
|
10
|
+
"test-temporal-element": TestTemporalElement;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export {};
|
|
@@ -12,6 +12,11 @@ interface TemporalState {
|
|
|
12
12
|
* Evaluates what the element's state should be based on the timeline
|
|
13
13
|
*/
|
|
14
14
|
export declare const evaluateTemporalState: (element: AnimatableElement) => TemporalState;
|
|
15
|
+
/**
|
|
16
|
+
* Evaluates element visibility specifically for animation coordination
|
|
17
|
+
* Uses inclusive end boundaries to prevent animation jumps at exact boundaries
|
|
18
|
+
*/
|
|
19
|
+
export declare const evaluateTemporalStateForAnimation: (element: AnimatableElement) => TemporalState;
|
|
15
20
|
/**
|
|
16
21
|
* Main function: synchronizes DOM element with timeline
|
|
17
22
|
*/
|