@editframe/elements 0.19.2-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 +7 -7
  28. package/dist/elements/EFTimegroup.js +59 -16
  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 +88 -18
  78. package/src/elements/updateAnimations.browsertest.ts +361 -12
  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;
@@ -33,12 +38,7 @@ export declare class EFTimegroup extends EFTimegroup_base {
33
38
  getPendingFrameTasks(signal?: AbortSignal): Promise<Task<readonly unknown[], unknown>[]>;
34
39
  waitForNestedUpdates(signal?: AbortSignal): Promise<void>;
35
40
  waitForFrameTasks(): Promise<void>;
36
- /**
37
- * Wait for all media elements to load their initial segments.
38
- * Ideally we would only need the extracted index json data, but
39
- * that caused issues with constructing audio data. We had negative durations
40
- * in calculations and it was not clear why.
41
- */
41
+ mediaDurationsPromise: Promise<void> | undefined;
42
42
  waitForMediaDurations(): Promise<void>;
43
43
  get childTemporals(): import('./EFTemporal.js').TemporalMixinInterface[];
44
44
  get contextProvider(): import('../gui/ContextMixin.js').ContextMixinInterface | null;
@@ -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";
@@ -32,6 +32,7 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
32
32
  this._mode = "contain";
33
33
  this._overlapMs = 0;
34
34
  this.fit = "none";
35
+ this.mediaDurationsPromise = void 0;
35
36
  this.frameTask = new Task(this, {
36
37
  autoRun: false,
37
38
  args: () => [this.ownCurrentTimeMs, this.currentTimeMs],
@@ -50,9 +51,10 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
50
51
  if (!this.isRootTimegroup) return;
51
52
  await this.waitForMediaDurations();
52
53
  const newTime = Math.max(0, Math.min(targetTime ?? 0, this.durationMs / 1e3));
54
+ this.#currentTime = newTime;
53
55
  this.requestUpdate("currentTime");
54
- await this.frameTask.run();
55
- this.#saveTimeToLocalStorage(newTime);
56
+ await this.runThrottledFrameTask();
57
+ this.#saveTimeToLocalStorage(this.#currentTime);
56
58
  this.#seekInProgress = false;
57
59
  return newTime;
58
60
  }
@@ -89,18 +91,45 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
89
91
  #seekInProgress = false;
90
92
  #pendingSeekTime;
91
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
+ }
92
122
  set currentTime(time) {
123
+ time = Math.max(0, time);
93
124
  if (!this.isRootTimegroup) return;
94
125
  if (Number.isNaN(time)) return;
95
126
  if (time === this.#currentTime && !this.#processingPendingSeek) return;
96
127
  if (this.#pendingSeekTime === time) return;
97
128
  if (this.#seekInProgress) {
98
- console.trace("pending seek to", time);
99
129
  this.#pendingSeekTime = time;
100
130
  this.#currentTime = time;
101
131
  return;
102
132
  }
103
- console.trace("seeking to", time);
104
133
  this.#currentTime = time;
105
134
  this.#seekInProgress = true;
106
135
  this.seekTask.run().finally(() => {
@@ -161,13 +190,23 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
161
190
  }
162
191
  connectedCallback() {
163
192
  super.connectedCallback();
164
- if (this.id) this.waitForMediaDurations().then(() => {
165
- const maybeLoadedTime = this.maybeLoadTimeFromLocalStorage();
166
- if (maybeLoadedTime !== void 0) this.currentTime = maybeLoadedTime;
193
+ this.waitForMediaDurations().then(() => {
194
+ if (this.id) {
195
+ const maybeLoadedTime = this.maybeLoadTimeFromLocalStorage();
196
+ if (maybeLoadedTime !== void 0) this.currentTime = maybeLoadedTime;
197
+ }
198
+ if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) this.seekTask.run();
167
199
  });
168
200
  if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
169
201
  if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
170
202
  }
203
+ #previousDurationMs = 0;
204
+ updated(_changedProperties) {
205
+ if (this.#previousDurationMs !== this.durationMs) {
206
+ this.#previousDurationMs = this.durationMs;
207
+ this.runThrottledFrameTask();
208
+ }
209
+ }
171
210
  disconnectedCallback() {
172
211
  super.disconnectedCallback();
173
212
  this.#resizeObserver?.disconnect();
@@ -225,7 +264,9 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
225
264
  const startTimeMs = temporal.startTimeMs;
226
265
  const endTimeMs = temporal.endTimeMs;
227
266
  const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
228
- 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;
229
270
  return elementStartsBeforeEnd && elementEndsAfterStart;
230
271
  });
231
272
  const frameTasks = activeTemporals.map((temporal) => temporal.frameTask);
@@ -249,12 +290,14 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
249
290
  async waitForFrameTasks() {
250
291
  const temporalElements = deepGetElementsWithFrameTasks(this);
251
292
  const visibleElements = temporalElements.filter((element) => {
252
- const temporalState = evaluateTemporalState(element);
253
- return temporalState.isVisible;
293
+ const animationState = evaluateTemporalStateForAnimation(element);
294
+ return animationState.isVisible;
254
295
  });
255
- await Promise.all(visibleElements.map((element) => {
256
- return element.frameTask.run();
257
- }));
296
+ await Promise.all(visibleElements.map((element) => element.frameTask.run()));
297
+ }
298
+ async waitForMediaDurations() {
299
+ if (!this.mediaDurationsPromise) this.mediaDurationsPromise = this.#waitForMediaDurations();
300
+ return this.mediaDurationsPromise;
258
301
  }
259
302
  /**
260
303
  * Wait for all media elements to load their initial segments.
@@ -262,7 +305,7 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
262
305
  * that caused issues with constructing audio data. We had negative durations
263
306
  * in calculations and it was not clear why.
264
307
  */
265
- async waitForMediaDurations() {
308
+ async #waitForMediaDurations() {
266
309
  await this.updateComplete;
267
310
  const mediaElements = deepGetMediaElements(this);
268
311
  await Promise.all(mediaElements.map((m) => m.mediaEngineTask.value ? Promise.resolve() : m.mediaEngineTask.run()));
@@ -408,4 +451,4 @@ _decorate([property({
408
451
  attribute: "currenttime"
409
452
  })], EFTimegroup.prototype, "currentTime", null);
410
453
  EFTimegroup = _EFTimegroup = _decorate([customElement("ef-timegroup")], EFTimegroup);
411
- 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 {};