@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.
Files changed (96) hide show
  1. package/dist/elements/ContextProxiesController.d.ts +40 -0
  2. package/dist/elements/ContextProxiesController.js +69 -0
  3. package/dist/elements/EFCaptions.d.ts +45 -6
  4. package/dist/elements/EFCaptions.js +220 -26
  5. package/dist/elements/EFImage.js +4 -1
  6. package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +2 -1
  7. package/dist/elements/EFMedia/AssetIdMediaEngine.js +9 -0
  8. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -0
  9. package/dist/elements/EFMedia/AssetMediaEngine.js +11 -0
  10. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +13 -1
  11. package/dist/elements/EFMedia/BaseMediaEngine.js +9 -0
  12. package/dist/elements/EFMedia/JitMediaEngine.d.ts +7 -1
  13. package/dist/elements/EFMedia/JitMediaEngine.js +24 -0
  14. package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
  15. package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
  16. package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
  17. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
  18. package/dist/elements/EFMedia.js +25 -1
  19. package/dist/elements/EFSurface.browsertest.d.ts +0 -0
  20. package/dist/elements/EFSurface.d.ts +30 -0
  21. package/dist/elements/EFSurface.js +96 -0
  22. package/dist/elements/EFTemporal.js +7 -6
  23. package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
  24. package/dist/elements/EFThumbnailStrip.d.ts +86 -0
  25. package/dist/elements/EFThumbnailStrip.js +490 -0
  26. package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
  27. package/dist/elements/EFTimegroup.d.ts +6 -1
  28. package/dist/elements/EFTimegroup.js +46 -10
  29. package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
  30. package/dist/elements/updateAnimations.d.ts +5 -0
  31. package/dist/elements/updateAnimations.js +37 -13
  32. package/dist/getRenderInfo.js +1 -1
  33. package/dist/gui/ContextMixin.js +27 -14
  34. package/dist/gui/EFControls.browsertest.d.ts +0 -0
  35. package/dist/gui/EFControls.d.ts +38 -0
  36. package/dist/gui/EFControls.js +51 -0
  37. package/dist/gui/EFFilmstrip.d.ts +40 -1
  38. package/dist/gui/EFFilmstrip.js +240 -3
  39. package/dist/gui/EFPreview.js +2 -1
  40. package/dist/gui/EFScrubber.d.ts +6 -5
  41. package/dist/gui/EFScrubber.js +31 -21
  42. package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
  43. package/dist/gui/EFTimeDisplay.d.ts +2 -6
  44. package/dist/gui/EFTimeDisplay.js +13 -23
  45. package/dist/gui/TWMixin.js +1 -1
  46. package/dist/gui/currentTimeContext.d.ts +3 -0
  47. package/dist/gui/currentTimeContext.js +3 -0
  48. package/dist/gui/durationContext.d.ts +3 -0
  49. package/dist/gui/durationContext.js +3 -0
  50. package/dist/index.d.ts +3 -0
  51. package/dist/index.js +4 -1
  52. package/dist/style.css +1 -1
  53. package/dist/transcoding/types/index.d.ts +11 -0
  54. package/dist/utils/LRUCache.d.ts +46 -0
  55. package/dist/utils/LRUCache.js +382 -1
  56. package/dist/utils/LRUCache.test.d.ts +1 -0
  57. package/package.json +2 -2
  58. package/src/elements/ContextProxiesController.ts +123 -0
  59. package/src/elements/EFCaptions.browsertest.ts +1820 -0
  60. package/src/elements/EFCaptions.ts +373 -36
  61. package/src/elements/EFImage.ts +4 -1
  62. package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
  63. package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
  64. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
  65. package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
  66. package/src/elements/EFMedia/JitMediaEngine.ts +48 -0
  67. package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
  68. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
  69. package/src/elements/EFMedia.ts +38 -1
  70. package/src/elements/EFSurface.browsertest.ts +155 -0
  71. package/src/elements/EFSurface.ts +141 -0
  72. package/src/elements/EFTemporal.ts +14 -8
  73. package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
  74. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
  75. package/src/elements/EFThumbnailStrip.ts +905 -0
  76. package/src/elements/EFTimegroup.browsertest.ts +56 -7
  77. package/src/elements/EFTimegroup.ts +70 -11
  78. package/src/elements/updateAnimations.browsertest.ts +333 -11
  79. package/src/elements/updateAnimations.ts +68 -19
  80. package/src/gui/ContextMixin.browsertest.ts +0 -25
  81. package/src/gui/ContextMixin.ts +44 -20
  82. package/src/gui/EFControls.browsertest.ts +175 -0
  83. package/src/gui/EFControls.ts +84 -0
  84. package/src/gui/EFFilmstrip.ts +323 -4
  85. package/src/gui/EFPreview.ts +2 -1
  86. package/src/gui/EFScrubber.ts +29 -25
  87. package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
  88. package/src/gui/EFTimeDisplay.ts +12 -40
  89. package/src/gui/currentTimeContext.ts +5 -0
  90. package/src/gui/durationContext.ts +3 -0
  91. package/src/transcoding/types/index.ts +13 -0
  92. package/src/utils/LRUCache.test.ts +272 -0
  93. package/src/utils/LRUCache.ts +543 -0
  94. package/types.json +1 -1
  95. package/dist/transcoding/cache/CacheManager.d.ts +0 -73
  96. 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 };
@@ -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 { evaluateTemporalState, updateAnimations } from "./updateAnimations.js";
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.frameTask.run();
56
- this.#saveTimeToLocalStorage(newTime);
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 elementEndsAfterStart = endTimeMs > timelineTimeMs;
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 temporalState = evaluateTemporalState(element);
256
- return temporalState.isVisible;
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
  */