@editframe/elements 0.31.0-beta.0 → 0.31.1-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/EFThumbnailStrip.d.ts +11 -0
- package/dist/elements/EFThumbnailStrip.js +95 -13
- package/dist/elements/EFThumbnailStrip.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +12 -0
- package/dist/elements/EFTimegroup.js +28 -1
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/SessionThumbnailCache.js +7 -5
- package/dist/elements/SessionThumbnailCache.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.js +2 -9
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/preview/workers/encoderWorkerInline.js +103 -0
- package/dist/preview/workers/encoderWorkerInline.js.map +1 -0
- package/dist/style.css +3 -0
- package/package.json +2 -2
|
@@ -48,6 +48,10 @@ declare class EFThumbnailStrip extends LitElement {
|
|
|
48
48
|
private _renderRequested;
|
|
49
49
|
/** Track if any thumbnails have been loaded (for ready event) */
|
|
50
50
|
private _hasLoadedThumbnails;
|
|
51
|
+
/** Track the last epoch we loaded thumbnails for */
|
|
52
|
+
private _lastLoadedEpoch;
|
|
53
|
+
/** Track layout parameters to avoid unnecessary slot recreation */
|
|
54
|
+
private _lastLayoutParams;
|
|
51
55
|
connectedCallback(): void;
|
|
52
56
|
disconnectedCallback(): void;
|
|
53
57
|
updated(changedProperties: Map<string | number | symbol, unknown>): void;
|
|
@@ -78,6 +82,11 @@ declare class EFThumbnailStrip extends LitElement {
|
|
|
78
82
|
private _onScroll;
|
|
79
83
|
private _onContextScroll;
|
|
80
84
|
private get _viewportWidth();
|
|
85
|
+
/**
|
|
86
|
+
* Watch for async content loading from child media elements.
|
|
87
|
+
* When media finishes loading, increment the epoch to invalidate cached thumbnails.
|
|
88
|
+
*/
|
|
89
|
+
private _watchChildContentLoading;
|
|
81
90
|
private _setupTargetObserver;
|
|
82
91
|
private _scheduleRender;
|
|
83
92
|
/**
|
|
@@ -86,6 +95,7 @@ declare class EFThumbnailStrip extends LitElement {
|
|
|
86
95
|
private _checkAndDispatchReady;
|
|
87
96
|
/**
|
|
88
97
|
* Calculate thumbnail layout based on current dimensions and time range.
|
|
98
|
+
* Only recreates slots if layout parameters have actually changed.
|
|
89
99
|
*/
|
|
90
100
|
private _calculateLayout;
|
|
91
101
|
/**
|
|
@@ -112,6 +122,7 @@ declare class EFThumbnailStrip extends LitElement {
|
|
|
112
122
|
private _drawThumbnailImage;
|
|
113
123
|
/**
|
|
114
124
|
* Load thumbnails that are visible in the current viewport.
|
|
125
|
+
* Skips loading if the epoch hasn't changed since last load.
|
|
115
126
|
*/
|
|
116
127
|
private _loadVisibleThumbnails;
|
|
117
128
|
/**
|
|
@@ -19,13 +19,17 @@ function isEFTimegroup(element) {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Get identifiers for cache key generation.
|
|
22
|
-
* Returns rootId (for cache isolation)
|
|
22
|
+
* Returns rootId (for cache isolation), elementId (for element-specific caching), and epoch (for content versioning).
|
|
23
23
|
*/
|
|
24
24
|
function getCacheIdentifiers(element) {
|
|
25
25
|
const rootTemporal = findRootTemporal(element);
|
|
26
|
+
const rootTimegroup = rootTemporal && isEFTimegroup(rootTemporal) ? rootTemporal : null;
|
|
27
|
+
const rootId = rootTimegroup?.id || "default";
|
|
28
|
+
const epoch = rootTimegroup?.contentEpoch ?? 0;
|
|
26
29
|
return {
|
|
27
|
-
rootId
|
|
28
|
-
elementId: isEFVideo(element) ? element.src || element.id || "video" : element.id || "timegroup"
|
|
30
|
+
rootId,
|
|
31
|
+
elementId: isEFVideo(element) ? element.src || element.id || "video" : element.id || "timegroup",
|
|
32
|
+
epoch
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
/** Padding in pixels for virtual rendering (render extra thumbnails beyond viewport) */
|
|
@@ -58,6 +62,8 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
58
62
|
this._captureInProgress = false;
|
|
59
63
|
this._renderRequested = false;
|
|
60
64
|
this._hasLoadedThumbnails = false;
|
|
65
|
+
this._lastLoadedEpoch = null;
|
|
66
|
+
this._lastLayoutParams = null;
|
|
61
67
|
this._onScroll = () => {
|
|
62
68
|
if (!this._scrollContainer) return;
|
|
63
69
|
this._currentScrollLeft = this._scrollContainer.scrollLeft;
|
|
@@ -96,7 +102,11 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
96
102
|
const oldValue = this._targetElement;
|
|
97
103
|
this._targetElement = value;
|
|
98
104
|
this._mutationObserver?.disconnect();
|
|
99
|
-
if (value !== oldValue)
|
|
105
|
+
if (value !== oldValue) {
|
|
106
|
+
this._hasLoadedThumbnails = false;
|
|
107
|
+
this._lastLoadedEpoch = null;
|
|
108
|
+
this._lastLayoutParams = null;
|
|
109
|
+
}
|
|
100
110
|
if (value && value !== oldValue) this._setupTargetObserver(value);
|
|
101
111
|
this.requestUpdate("targetElement", oldValue);
|
|
102
112
|
}
|
|
@@ -222,6 +232,31 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
222
232
|
if (this._scrollContainer) return this._scrollContainer.clientWidth - this._trackLeftOffset;
|
|
223
233
|
return this._width;
|
|
224
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Watch for async content loading from child media elements.
|
|
237
|
+
* When media finishes loading, increment the epoch to invalidate cached thumbnails.
|
|
238
|
+
*/
|
|
239
|
+
_watchChildContentLoading(target) {
|
|
240
|
+
const mediaElements = target.querySelectorAll("ef-video, ef-image, ef-audio");
|
|
241
|
+
for (const el of mediaElements) {
|
|
242
|
+
const mediaEngine = el.mediaEngineTask;
|
|
243
|
+
if (mediaEngine?.taskComplete) mediaEngine.taskComplete.then(() => {
|
|
244
|
+
if (this._targetElement === target) {
|
|
245
|
+
target.incrementContentEpoch();
|
|
246
|
+
this._lastLayoutParams = null;
|
|
247
|
+
this._scheduleRender();
|
|
248
|
+
}
|
|
249
|
+
}).catch(() => {});
|
|
250
|
+
const fetchTask = el.fetchImage;
|
|
251
|
+
if (fetchTask?.taskComplete) fetchTask.taskComplete.then(() => {
|
|
252
|
+
if (this._targetElement === target) {
|
|
253
|
+
target.incrementContentEpoch();
|
|
254
|
+
this._lastLayoutParams = null;
|
|
255
|
+
this._scheduleRender();
|
|
256
|
+
}
|
|
257
|
+
}).catch(() => {});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
225
260
|
_setupTargetObserver(target) {
|
|
226
261
|
if (isEFVideo(target)) {
|
|
227
262
|
this._mutationObserver = new MutationObserver(() => this._scheduleRender());
|
|
@@ -243,11 +278,37 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
243
278
|
});
|
|
244
279
|
});
|
|
245
280
|
} else if (isEFTimegroup(target)) {
|
|
246
|
-
this._mutationObserver = new MutationObserver(() =>
|
|
281
|
+
this._mutationObserver = new MutationObserver((mutations) => {
|
|
282
|
+
if (mutations.some((mutation) => {
|
|
283
|
+
if (mutation.type === "childList") return mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0;
|
|
284
|
+
if (mutation.type === "attributes") {
|
|
285
|
+
const attrName = mutation.attributeName;
|
|
286
|
+
if (attrName === "currenttime" || attrName === "current-time" || attrName === "playing" || attrName === "loop") return false;
|
|
287
|
+
return attrName === "src" || attrName === "asset-id" || attrName === "style" || attrName === "transform";
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
})) {
|
|
291
|
+
const epochBefore = target.contentEpoch;
|
|
292
|
+
target.incrementContentEpoch();
|
|
293
|
+
if (target.contentEpoch !== epochBefore) {
|
|
294
|
+
this._lastLayoutParams = null;
|
|
295
|
+
if (mutations.some((m) => m.addedNodes.length > 0)) this._watchChildContentLoading(target);
|
|
296
|
+
this._scheduleRender();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
247
300
|
this._mutationObserver.observe(target, {
|
|
248
301
|
childList: true,
|
|
249
|
-
subtree: true
|
|
302
|
+
subtree: true,
|
|
303
|
+
attributes: true,
|
|
304
|
+
attributeFilter: [
|
|
305
|
+
"src",
|
|
306
|
+
"asset-id",
|
|
307
|
+
"style",
|
|
308
|
+
"transform"
|
|
309
|
+
]
|
|
250
310
|
});
|
|
311
|
+
this._watchChildContentLoading(target);
|
|
251
312
|
if (target.durationMs === 0) {
|
|
252
313
|
const checkDuration = () => {
|
|
253
314
|
if (this._targetElement !== target) return;
|
|
@@ -285,19 +346,32 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
285
346
|
}
|
|
286
347
|
/**
|
|
287
348
|
* Calculate thumbnail layout based on current dimensions and time range.
|
|
349
|
+
* Only recreates slots if layout parameters have actually changed.
|
|
288
350
|
*/
|
|
289
351
|
_calculateLayout() {
|
|
290
352
|
if (this._width <= 0 || this._height <= 0 || !this._targetElement) {
|
|
291
353
|
this._thumbnailSlots = [];
|
|
354
|
+
this._lastLayoutParams = null;
|
|
292
355
|
return;
|
|
293
356
|
}
|
|
294
357
|
const timeRange = this._getTimeRange();
|
|
295
358
|
if (timeRange.endMs <= timeRange.startMs) {
|
|
296
359
|
this._thumbnailSlots = [];
|
|
360
|
+
this._lastLayoutParams = null;
|
|
297
361
|
return;
|
|
298
362
|
}
|
|
299
363
|
const thumbWidth = this._getEffectiveThumbnailWidth();
|
|
300
364
|
const gap = this.gap;
|
|
365
|
+
const currentParams = {
|
|
366
|
+
width: this._width,
|
|
367
|
+
height: this._height,
|
|
368
|
+
startTimeMs: timeRange.startMs,
|
|
369
|
+
endTimeMs: timeRange.endMs,
|
|
370
|
+
thumbWidth,
|
|
371
|
+
gap
|
|
372
|
+
};
|
|
373
|
+
if (this._lastLayoutParams && this._lastLayoutParams.width === currentParams.width && this._lastLayoutParams.height === currentParams.height && this._lastLayoutParams.startTimeMs === currentParams.startTimeMs && this._lastLayoutParams.endTimeMs === currentParams.endTimeMs && this._lastLayoutParams.thumbWidth === currentParams.thumbWidth && this._lastLayoutParams.gap === currentParams.gap) return;
|
|
374
|
+
this._lastLayoutParams = currentParams;
|
|
301
375
|
const count = Math.max(1, Math.floor((this._width + gap) / (thumbWidth + gap)));
|
|
302
376
|
const pitch = count > 1 ? (this._width - thumbWidth) / (count - 1) : 0;
|
|
303
377
|
const slots = [];
|
|
@@ -355,13 +429,13 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
355
429
|
*/
|
|
356
430
|
_checkCache() {
|
|
357
431
|
if (!this._targetElement) return;
|
|
358
|
-
const { rootId, elementId } = getCacheIdentifiers(this._targetElement);
|
|
432
|
+
const { rootId, elementId, epoch } = getCacheIdentifiers(this._targetElement);
|
|
359
433
|
for (const slot of this._thumbnailSlots) {
|
|
360
|
-
const key = getCacheKey(rootId, elementId, slot.timeMs);
|
|
434
|
+
const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);
|
|
361
435
|
if (sessionThumbnailCache.has(key)) {
|
|
362
436
|
slot.imageData = sessionThumbnailCache.get(key);
|
|
363
437
|
slot.status = "cached";
|
|
364
|
-
}
|
|
438
|
+
} else slot.status = "pending";
|
|
365
439
|
}
|
|
366
440
|
}
|
|
367
441
|
/**
|
|
@@ -447,9 +521,16 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
447
521
|
}
|
|
448
522
|
/**
|
|
449
523
|
* Load thumbnails that are visible in the current viewport.
|
|
524
|
+
* Skips loading if the epoch hasn't changed since last load.
|
|
450
525
|
*/
|
|
451
526
|
async _loadVisibleThumbnails() {
|
|
452
527
|
if (this._captureInProgress || !this._targetElement) return;
|
|
528
|
+
if (isEFTimegroup(this._targetElement)) {
|
|
529
|
+
const currentEpoch = this._targetElement.contentEpoch;
|
|
530
|
+
if (this._lastLoadedEpoch !== null && this._lastLoadedEpoch === currentEpoch) {
|
|
531
|
+
if (!this._thumbnailSlots.some((s) => s.status === "pending")) return;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
453
534
|
const viewportWidth = this._viewportWidth;
|
|
454
535
|
const scrollOffset = this._currentScrollLeft;
|
|
455
536
|
const stripWidth = this._width;
|
|
@@ -475,6 +556,7 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
475
556
|
} finally {
|
|
476
557
|
this._captureInProgress = false;
|
|
477
558
|
this._drawCanvas();
|
|
559
|
+
if (isEFTimegroup(this._targetElement)) this._lastLoadedEpoch = this._targetElement.contentEpoch;
|
|
478
560
|
if (this._thumbnailSlots.some((s) => s.status === "cached") && !this._hasLoadedThumbnails) {
|
|
479
561
|
this._hasLoadedThumbnails = true;
|
|
480
562
|
this.dispatchEvent(new CustomEvent("thumbnails-ready", { bubbles: true }));
|
|
@@ -486,7 +568,7 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
486
568
|
*/
|
|
487
569
|
async _captureTimegroupThumbnails(slots) {
|
|
488
570
|
const target = this._targetElement;
|
|
489
|
-
const { rootId, elementId } = getCacheIdentifiers(target);
|
|
571
|
+
const { rootId, elementId, epoch } = getCacheIdentifiers(target);
|
|
490
572
|
const timegroupWidth = target.offsetWidth || 1920;
|
|
491
573
|
const timegroupHeight = target.offsetHeight || 1080;
|
|
492
574
|
const scale = Math.min(1, this._height / timegroupHeight, MAX_CAPTURE_WIDTH / timegroupWidth);
|
|
@@ -504,7 +586,7 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
504
586
|
if (canvas) {
|
|
505
587
|
const imageData = this._canvasToImageData(canvas);
|
|
506
588
|
if (imageData) {
|
|
507
|
-
const key = getCacheKey(rootId, elementId, slot.timeMs);
|
|
589
|
+
const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);
|
|
508
590
|
sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);
|
|
509
591
|
slot.imageData = imageData;
|
|
510
592
|
slot.status = "cached";
|
|
@@ -523,7 +605,7 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
523
605
|
*/
|
|
524
606
|
async _captureVideoThumbnails(slots) {
|
|
525
607
|
const target = this._targetElement;
|
|
526
|
-
const { rootId, elementId } = getCacheIdentifiers(target);
|
|
608
|
+
const { rootId, elementId, epoch } = getCacheIdentifiers(target);
|
|
527
609
|
if (target.mediaEngineTask) await target.mediaEngineTask.taskComplete;
|
|
528
610
|
const mediaEngine = target.mediaEngineTask?.value;
|
|
529
611
|
if (!mediaEngine) return;
|
|
@@ -540,7 +622,7 @@ let EFThumbnailStrip = class EFThumbnailStrip$1 extends LitElement {
|
|
|
540
622
|
if (result?.thumbnail) {
|
|
541
623
|
const imageData = this._canvasToImageData(result.thumbnail);
|
|
542
624
|
if (imageData) {
|
|
543
|
-
const key = getCacheKey(rootId, elementId, slot.timeMs);
|
|
625
|
+
const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);
|
|
544
626
|
sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);
|
|
545
627
|
slot.imageData = imageData;
|
|
546
628
|
slot.status = "cached";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFThumbnailStrip.js","names":["EFThumbnailStrip","node: Node | null","parentNode: Node | null","slots: ThumbnailSlot[]"],"sources":["../../src/elements/EFThumbnailStrip.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport type { EFVideo } from \"./EFVideo.js\";\nimport type { EFTimegroup } from \"./EFTimegroup.js\";\nimport { TargetController } from \"./TargetController.ts\";\nimport { timelineStateContext, type TimelineState } from \"../gui/timeline/timelineStateContext.js\";\nimport { sessionThumbnailCache, getCacheKey } from \"./SessionThumbnailCache.js\";\nimport { findRootTemporal } from \"./findRootTemporal.js\";\n\n/** Type guard to check if element is EFVideo */\nfunction isEFVideo(element: Element | null): element is EFVideo {\n return element?.tagName.toLowerCase() === \"ef-video\";\n}\n\n/** Type guard to check if element is EFTimegroup */\nfunction isEFTimegroup(element: Element | null): element is EFTimegroup {\n return element?.tagName.toLowerCase() === \"ef-timegroup\";\n}\n\n/**\n * Get identifiers for cache key generation.\n * Returns rootId (for cache isolation) and elementId (for element-specific caching).\n */\nfunction getCacheIdentifiers(element: EFVideo | EFTimegroup): { rootId: string; elementId: string } {\n // Get root timegroup for cache isolation between projects\n const rootTemporal = findRootTemporal(element);\n const rootTimegroup = rootTemporal && isEFTimegroup(rootTemporal) ? rootTemporal : null;\n const rootId = rootTimegroup?.id || \"default\";\n\n // Element identifier\n const elementId = isEFVideo(element)\n ? element.src || element.id || \"video\"\n : element.id || \"timegroup\";\n\n return { rootId, elementId };\n}\n\n/** Padding in pixels for virtual rendering (render extra thumbnails beyond viewport) */\nconst VIRTUAL_RENDER_PADDING_PX = 200;\n\n/** Default gap between thumbnails */\nconst DEFAULT_GAP = 4;\n\n/** Default aspect ratio if unknown */\nconst DEFAULT_ASPECT_RATIO = 16 / 9;\n\n/** Max canvas width for thumbnail captures */\nconst MAX_CAPTURE_WIDTH = 480;\n\n/** Thumbnails to capture per batch */\nconst BATCH_SIZE = 10;\n\ninterface ThumbnailSlot {\n timeMs: number;\n x: number; // Absolute position (not scroll-adjusted)\n width: number;\n imageData?: ImageData;\n status: \"cached\" | \"loading\" | \"pending\";\n}\n\n@customElement(\"ef-thumbnail-strip\")\nexport class EFThumbnailStrip extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n background: #1a1a2e;\n overflow: hidden;\n width: 100%;\n height: 100%;\n }\n canvas {\n display: block;\n /* Absolute positioning - we manually position at visible region */\n position: absolute;\n top: 0;\n /* Left and width set programmatically based on visible portion */\n height: 100%;\n image-rendering: auto;\n }\n `,\n ];\n\n canvasRef = createRef<HTMLCanvasElement>();\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public Properties\n // ─────────────────────────────────────────────────────────────────────────\n\n @property({ type: String })\n target = \"\";\n\n @property({ type: Number, attribute: \"thumbnail-width\" })\n thumbnailWidth = 0; // 0 = auto (calculate from height using aspect ratio)\n\n @property({ type: Number, attribute: \"gap\" })\n gap = DEFAULT_GAP;\n\n @property({ type: Number, attribute: \"start-time-ms\" })\n startTimeMs?: number;\n\n @property({ type: Number, attribute: \"end-time-ms\" })\n endTimeMs?: number;\n\n @property({\n type: Boolean,\n attribute: \"use-intrinsic-duration\",\n reflect: true,\n converter: {\n fromAttribute: (value: string | null) => value === \"true\",\n toAttribute: (value: boolean) => (value ? \"true\" : null),\n },\n })\n useIntrinsicDuration = false;\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs = 0.1;\n\n // ─────────────────────────────────────────────────────────────────────────\n // Internal State\n // ─────────────────────────────────────────────────────────────────────────\n\n /** Timeline state context for viewport info */\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n private _timelineState?: TimelineState;\n\n /** Target element controller */\n // @ts-expect-error controller used for side effects\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for side effects\n private _targetController: TargetController = new TargetController(this as any);\n\n private _targetElement: EFVideo | EFTimegroup | null = null;\n\n @state()\n get targetElement(): EFVideo | EFTimegroup | null {\n return this._targetElement;\n }\n\n set targetElement(value: EFVideo | EFTimegroup | null) {\n const oldValue = this._targetElement;\n this._targetElement = value;\n\n // Clean up old observer\n this._mutationObserver?.disconnect();\n\n // Reset ready state when target changes\n if (value !== oldValue) {\n this._hasLoadedThumbnails = false;\n }\n\n if (value && value !== oldValue) {\n this._setupTargetObserver(value);\n }\n\n this.requestUpdate(\"targetElement\", oldValue);\n }\n\n /** Host element dimensions */\n private _width = 0;\n private _height = 0;\n\n /** Scroll container reference */\n private _scrollContainer: HTMLElement | null = null;\n private _currentScrollLeft = 0;\n \n /** \n * Offset from scroll container's left edge to this element's track.\n * Used for sticky positioning when track doesn't start at x=0 (e.g., labels column).\n */\n private _trackLeftOffset = 0;\n\n /** Current thumbnail slots */\n private _thumbnailSlots: ThumbnailSlot[] = [];\n\n /** Capture in progress flag */\n private _captureInProgress = false;\n\n /** Resize observer */\n private _resizeObserver?: ResizeObserver;\n\n /** Mutation observer for target element changes */\n private _mutationObserver?: MutationObserver;\n\n /** Animation frame for scroll updates */\n private _scrollFrame?: number;\n\n /** Render request tracking */\n private _renderRequested = false;\n\n /** Track if any thumbnails have been loaded (for ready event) */\n private _hasLoadedThumbnails = false;\n\n // ─────────────────────────────────────────────────────────────────────────\n // Lifecycle\n // ─────────────────────────────────────────────────────────────────────────\n\n connectedCallback() {\n super.connectedCallback();\n\n // Set up resize observer\n this._resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const box = entry.borderBoxSize?.[0];\n this._width = box?.inlineSize ?? entry.contentRect.width;\n this._height = box?.blockSize ?? entry.contentRect.height;\n // Recalculate track offset in case layout changed\n this._calculateTrackOffset();\n this._scheduleRender();\n }\n });\n this._resizeObserver.observe(this);\n\n // Find scroll container after element is ready\n this.updateComplete.then(() => {\n this._findScrollContainer();\n this._scheduleRender();\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this._resizeObserver?.disconnect();\n this._mutationObserver?.disconnect();\n this._detachScrollListener();\n\n if (this._scrollFrame) {\n cancelAnimationFrame(this._scrollFrame);\n }\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated(changedProperties);\n\n // Re-render on property changes\n if (\n changedProperties.has(\"thumbnailWidth\") ||\n changedProperties.has(\"gap\") ||\n changedProperties.has(\"startTimeMs\") ||\n changedProperties.has(\"endTimeMs\") ||\n changedProperties.has(\"useIntrinsicDuration\") ||\n changedProperties.has(\"pixelsPerMs\") ||\n changedProperties.has(\"targetElement\")\n ) {\n this._scheduleRender();\n }\n\n // Handle timeline context scroll changes\n if (changedProperties.has(\"_timelineState\")) {\n this._onContextScroll();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Scroll Handling\n // ─────────────────────────────────────────────────────────────────────────\n\n private _findScrollContainer(): void {\n // Walk up the DOM tree, crossing shadow boundaries\n let node: Node | null = this.parentNode;\n \n while (node) {\n // Check if this node is an element with overflow-x: auto or scroll\n if (node instanceof HTMLElement) {\n const style = getComputedStyle(node);\n if (style.overflowX === \"auto\" || style.overflowX === \"scroll\") {\n this._scrollContainer = node;\n this._calculateTrackOffset();\n this._attachScrollListener();\n return;\n }\n }\n \n // Move to parent, crossing shadow DOM boundaries\n if (node.parentNode) {\n node = node.parentNode;\n } else if (node instanceof ShadowRoot) {\n // Cross shadow boundary to the host element\n node = node.host;\n } else {\n break;\n }\n }\n }\n\n /**\n * Calculate the horizontal offset from scroll container's left edge to this element's track.\n * This accounts for sticky labels or other elements that precede the track area.\n * \n * We look for our specific timeline elements (ef-timeline-row) and measure their label width.\n */\n private _calculateTrackOffset(): void {\n if (!this._scrollContainer) {\n this._trackLeftOffset = 0;\n return;\n }\n \n // Find ef-timeline-row ancestor and get its label width\n const timelineRow = this._findTimelineRow();\n if (timelineRow) {\n const labelWidth = this._getTimelineRowLabelWidth(timelineRow);\n if (labelWidth > 0) {\n this._trackLeftOffset = labelWidth;\n return;\n }\n }\n \n // No timeline row found - track starts at scroll container's left edge\n this._trackLeftOffset = 0;\n }\n \n /**\n * Find the ef-timeline-row ancestor by walking up through shadow DOM boundaries.\n */\n private _findTimelineRow(): Element | null {\n let node: Node | null = this;\n \n while (node) {\n // Check if this is ef-timeline-row\n if (node instanceof Element && node.tagName.toLowerCase() === 'ef-timeline-row') {\n return node;\n }\n \n // Move up through shadow DOM boundaries\n const parentNode: Node | null = node.parentNode;\n if (parentNode instanceof ShadowRoot) {\n node = parentNode.host;\n } else {\n node = parentNode;\n }\n }\n \n return null;\n }\n \n /**\n * Get the label width from an ef-timeline-row element.\n * Queries the shadow root for .row-label and returns its width.\n */\n private _getTimelineRowLabelWidth(timelineRow: Element): number {\n const shadowRoot = timelineRow.shadowRoot;\n if (!shadowRoot) return 0;\n \n const rowLabel = shadowRoot.querySelector('.row-label');\n if (!rowLabel) return 0;\n \n return rowLabel.getBoundingClientRect().width;\n }\n \n /**\n * Get this strip's absolute position in the timeline (pixels from timeline origin).\n * Uses the target element's startTimeMs to determine position.\n */\n private _getStripTimelinePosition(): number {\n const target = this._targetElement;\n if (!target) return 0;\n \n // For videos, use their startTimeMs to get timeline position\n if (isEFVideo(target)) {\n const startTimeMs = target.startTimeMs ?? 0;\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? 0.1;\n return startTimeMs * pixelsPerMs;\n }\n \n // For root timegroup, position is 0\n return 0;\n }\n\n private _attachScrollListener(): void {\n if (!this._scrollContainer) return;\n this._scrollContainer.addEventListener(\"scroll\", this._onScroll, { passive: true });\n this._currentScrollLeft = this._scrollContainer.scrollLeft;\n }\n\n private _detachScrollListener(): void {\n if (this._scrollContainer) {\n this._scrollContainer.removeEventListener(\"scroll\", this._onScroll);\n this._scrollContainer = null;\n }\n }\n\n private _onScroll = (): void => {\n if (!this._scrollContainer) return;\n this._currentScrollLeft = this._scrollContainer.scrollLeft;\n this._drawCanvas();\n\n // Schedule loading of newly visible thumbnails\n if (!this._scrollFrame) {\n this._scrollFrame = requestAnimationFrame(() => {\n this._scrollFrame = undefined;\n this._loadVisibleThumbnails();\n });\n }\n };\n\n private _onContextScroll(): void {\n if (!this._timelineState || this._scrollContainer) return;\n this._currentScrollLeft = this._timelineState.viewportScrollLeft;\n this._drawCanvas();\n }\n\n private get _viewportWidth(): number {\n if (this._timelineState?.viewportWidth) {\n return this._timelineState.viewportWidth;\n }\n if (this._scrollContainer) {\n // Subtract track offset to get actual viewport width available for the track\n return this._scrollContainer.clientWidth - this._trackLeftOffset;\n }\n return this._width;\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Target Observer\n // ─────────────────────────────────────────────────────────────────────────\n\n private _setupTargetObserver(target: EFVideo | EFTimegroup): void {\n if (isEFVideo(target)) {\n // Watch video property changes\n this._mutationObserver = new MutationObserver(() => this._scheduleRender());\n this._mutationObserver.observe(target, {\n attributes: true,\n attributeFilter: [\"trimstart\", \"trimend\", \"sourcein\", \"sourceout\", \"src\"],\n });\n\n // Wait for media engine\n target.updateComplete.then(() => {\n if (this._targetElement !== target) return;\n target.mediaEngineTask?.taskComplete.then(() => {\n if (this._targetElement !== target) return;\n this._scheduleRender();\n });\n });\n } else if (isEFTimegroup(target)) {\n // Watch timegroup structure changes\n this._mutationObserver = new MutationObserver(() => this._scheduleRender());\n this._mutationObserver.observe(target, {\n childList: true,\n subtree: true,\n });\n\n // Watch for duration becoming available\n if (target.durationMs === 0) {\n const checkDuration = () => {\n if (this._targetElement !== target) return;\n if (target.durationMs > 0) {\n this._scheduleRender();\n } else {\n requestAnimationFrame(checkDuration);\n }\n };\n requestAnimationFrame(checkDuration);\n }\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Rendering Pipeline\n // ─────────────────────────────────────────────────────────────────────────\n\n private _scheduleRender(): void {\n if (this._renderRequested) return;\n this._renderRequested = true;\n\n requestAnimationFrame(() => {\n this._renderRequested = false;\n this._calculateLayout();\n this._checkCache();\n this._drawCanvas();\n this._loadVisibleThumbnails();\n\n // Check if we should dispatch ready event\n // (e.g., all thumbnails were already cached, or nothing to load)\n this._checkAndDispatchReady();\n });\n }\n\n /**\n * Check if thumbnails are ready and dispatch event if not already done.\n */\n private _checkAndDispatchReady(): void {\n if (this._hasLoadedThumbnails) return;\n\n // Consider ready if we have layout and either:\n // 1. Have some cached thumbnails, or\n // 2. Have no pending thumbnails (nothing to load)\n const hasLayout = this._thumbnailSlots.length > 0;\n const hasAnyCached = this._thumbnailSlots.some((s) => s.status === \"cached\");\n const hasPending = this._thumbnailSlots.some((s) => s.status === \"pending\");\n\n if (hasLayout && (hasAnyCached || !hasPending)) {\n this._hasLoadedThumbnails = true;\n this.dispatchEvent(new CustomEvent(\"thumbnails-ready\", { bubbles: true }));\n }\n }\n\n /**\n * Calculate thumbnail layout based on current dimensions and time range.\n */\n private _calculateLayout(): void {\n if (this._width <= 0 || this._height <= 0 || !this._targetElement) {\n this._thumbnailSlots = [];\n return;\n }\n\n const timeRange = this._getTimeRange();\n if (timeRange.endMs <= timeRange.startMs) {\n this._thumbnailSlots = [];\n return;\n }\n\n // Calculate thumbnail dimensions\n const thumbWidth = this._getEffectiveThumbnailWidth();\n const gap = this.gap;\n\n // Calculate how many thumbnails fit\n const count = Math.max(1, Math.floor((this._width + gap) / (thumbWidth + gap)));\n\n // Calculate pitch (spacing) for edge-to-edge fill\n const pitch = count > 1 ? (this._width - thumbWidth) / (count - 1) : 0;\n\n // Generate slots with timestamps\n const slots: ThumbnailSlot[] = [];\n const duration = timeRange.endMs - timeRange.startMs;\n\n for (let i = 0; i < count; i++) {\n const timeMs = count === 1\n ? (timeRange.startMs + timeRange.endMs) / 2\n : timeRange.startMs + (i * duration) / (count - 1);\n\n slots.push({\n timeMs,\n x: Math.round(i * pitch),\n width: thumbWidth,\n status: \"pending\",\n });\n }\n\n this._thumbnailSlots = slots;\n }\n\n /**\n * Get effective time range for thumbnails.\n */\n private _getTimeRange(): { startMs: number; endMs: number } {\n const target = this._targetElement;\n if (!target) return { startMs: 0, endMs: 0 };\n\n if (isEFVideo(target)) {\n if (this.useIntrinsicDuration) {\n // Intrinsic mode: 0 to full source duration\n return {\n startMs: this.startTimeMs ?? 0,\n endMs: this.endTimeMs ?? target.intrinsicDurationMs ?? 0,\n };\n }\n // Trimmed mode: source coordinates\n const sourceStart = target.sourceStartMs ?? 0;\n const trimmedDuration = target.durationMs ?? 0;\n return {\n startMs: this.startTimeMs !== undefined ? sourceStart + this.startTimeMs : sourceStart,\n endMs: this.endTimeMs !== undefined ? sourceStart + this.endTimeMs : sourceStart + trimmedDuration,\n };\n }\n\n // Timegroup\n return {\n startMs: this.startTimeMs ?? 0,\n endMs: (this.endTimeMs && this.endTimeMs > 0) ? this.endTimeMs : target.durationMs ?? 0,\n };\n }\n\n /**\n * Calculate effective thumbnail width (auto or specified).\n */\n private _getEffectiveThumbnailWidth(): number {\n if (this.thumbnailWidth > 0) return this.thumbnailWidth;\n\n const target = this._targetElement;\n let aspectRatio = DEFAULT_ASPECT_RATIO;\n\n if (isEFVideo(target)) {\n const w = (target as any).videoWidth || 1920;\n const h = (target as any).videoHeight || 1080;\n aspectRatio = w / h;\n } else if (isEFTimegroup(target)) {\n const w = target.offsetWidth || 1920;\n const h = target.offsetHeight || 1080;\n aspectRatio = w / h;\n }\n\n return Math.round(this._height * aspectRatio);\n }\n\n /**\n * Check cache for existing thumbnails.\n */\n private _checkCache(): void {\n if (!this._targetElement) return;\n\n const { rootId, elementId } = getCacheIdentifiers(this._targetElement);\n\n for (const slot of this._thumbnailSlots) {\n const key = getCacheKey(rootId, elementId, slot.timeMs);\n if (sessionThumbnailCache.has(key)) {\n slot.imageData = sessionThumbnailCache.get(key);\n slot.status = \"cached\";\n }\n }\n }\n\n /**\n * Draw the canvas with current thumbnail state.\n * Canvas is absolutely positioned at the visible portion of the strip.\n * Uses virtual rendering - only draws thumbnails in the visible region.\n */\n private _drawCanvas(): void {\n const canvas = this.canvasRef.value;\n if (!canvas) return;\n\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return;\n\n const stripWidth = this._width;\n const height = this._height;\n\n if (stripWidth <= 0 || height <= 0) return;\n\n const dpr = window.devicePixelRatio || 1;\n \n // Get scroll and viewport info\n const scrollLeft = this._currentScrollLeft;\n const viewportWidth = this._viewportWidth;\n \n // Get this strip's absolute position in the timeline\n const stripStartPx = this._getStripTimelinePosition();\n const stripEndPx = stripStartPx + stripWidth;\n \n // Calculate visible region in timeline coordinates (with padding)\n const visibleLeftPx = scrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx = scrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n \n // Check if strip is visible at all\n if (stripEndPx < visibleLeftPx || stripStartPx > visibleRightPx) {\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n \n // Calculate the intersection: what part of the strip is visible\n // Coordinates relative to strip's left edge (0 = strip start)\n const visibleStartInStrip = Math.max(0, visibleLeftPx - stripStartPx);\n const visibleEndInStrip = Math.min(stripWidth, visibleRightPx - stripStartPx);\n const visibleWidthPx = visibleEndInStrip - visibleStartInStrip;\n \n if (visibleWidthPx <= 0) {\n canvas.style.display = \"none\";\n return;\n }\n\n // Set canvas size with DPR\n const targetWidth = Math.ceil(visibleWidthPx * dpr);\n const targetHeight = Math.ceil(height * dpr);\n\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n }\n \n // Position canvas at the visible portion within the strip\n canvas.style.left = `${visibleStartInStrip}px`;\n canvas.style.width = `${visibleWidthPx}px`;\n canvas.style.height = `${height}px`;\n\n // Reset transform\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n // Clear with background\n ctx.fillStyle = \"#1a1a2e\";\n ctx.fillRect(0, 0, visibleWidthPx, height);\n\n // Draw each visible thumbnail\n for (const slot of this._thumbnailSlots) {\n const slotRight = slot.x + slot.width;\n\n // Skip if slot is outside visible strip region\n if (slotRight < visibleStartInStrip || slot.x > visibleEndInStrip) continue;\n\n // Draw position relative to canvas (canvas starts at visibleStartInStrip)\n const drawX = slot.x - visibleStartInStrip;\n\n // Skip if outside canvas bounds\n if (drawX + slot.width < 0 || drawX > visibleWidthPx) continue;\n\n if (slot.imageData) {\n this._drawThumbnailImage(ctx, slot.imageData, drawX, slot.width, height);\n } else {\n // Placeholder\n ctx.fillStyle = slot.status === \"loading\" ? \"#2d2d50\" : \"#2d2d44\";\n ctx.fillRect(drawX, 0, slot.width, height);\n\n // Loading indicator\n if (slot.status === \"loading\") {\n ctx.fillStyle = \"rgba(59, 130, 246, 0.3)\";\n ctx.fillRect(drawX, 0, slot.width, 2);\n }\n }\n }\n }\n\n /**\n * Draw a thumbnail image with cover mode scaling.\n */\n private _drawThumbnailImage(\n ctx: CanvasRenderingContext2D,\n imageData: ImageData,\n x: number,\n width: number,\n height: number,\n ): void {\n // Create temp canvas for ImageData\n const tempCanvas = document.createElement(\"canvas\");\n tempCanvas.width = imageData.width;\n tempCanvas.height = imageData.height;\n const tempCtx = tempCanvas.getContext(\"2d\");\n if (!tempCtx) return;\n tempCtx.putImageData(imageData, 0, 0);\n\n // Cover mode: crop to fill destination\n const srcAspect = imageData.width / imageData.height;\n const dstAspect = width / height;\n\n let srcX = 0, srcY = 0, srcW = imageData.width, srcH = imageData.height;\n\n if (srcAspect > dstAspect) {\n srcW = imageData.height * dstAspect;\n srcX = (imageData.width - srcW) / 2;\n } else {\n srcH = imageData.width / dstAspect;\n srcY = (imageData.height - srcH) / 2;\n }\n\n ctx.drawImage(tempCanvas, srcX, srcY, srcW, srcH, x, 0, width, height);\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Thumbnail Loading\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Load thumbnails that are visible in the current viewport.\n */\n private async _loadVisibleThumbnails(): Promise<void> {\n if (this._captureInProgress || !this._targetElement) return;\n\n const viewportWidth = this._viewportWidth;\n const scrollOffset = this._currentScrollLeft;\n const stripWidth = this._width;\n \n // Get strip's timeline position\n const stripStartPx = this._getStripTimelinePosition();\n \n // Calculate visible region in timeline coordinates\n const visibleLeftPx = scrollOffset - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx = scrollOffset + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n \n // Convert to strip-local coordinates\n const visibleStartInStrip = Math.max(0, visibleLeftPx - stripStartPx);\n const visibleEndInStrip = Math.min(stripWidth, visibleRightPx - stripStartPx);\n\n // Find pending slots in visible range (using strip-local coordinates)\n const pending = this._thumbnailSlots.filter((slot) => {\n if (slot.status !== \"pending\") return false;\n const slotRight = slot.x + slot.width;\n return slotRight >= visibleStartInStrip && slot.x <= visibleEndInStrip;\n });\n\n if (pending.length === 0) return;\n\n this._captureInProgress = true;\n\n // Mark as loading\n for (const slot of pending) {\n slot.status = \"loading\";\n }\n this._drawCanvas();\n\n try {\n if (isEFTimegroup(this._targetElement)) {\n await this._captureTimegroupThumbnails(pending);\n } else if (isEFVideo(this._targetElement)) {\n await this._captureVideoThumbnails(pending);\n }\n } catch (error) {\n console.warn(\"Failed to capture thumbnails:\", error);\n // Reset failed slots\n for (const slot of pending) {\n if (slot.status === \"loading\") {\n slot.status = \"pending\";\n }\n }\n } finally {\n this._captureInProgress = false;\n this._drawCanvas();\n\n // Dispatch ready event when thumbnails are first loaded\n const hasAnyLoaded = this._thumbnailSlots.some((s) => s.status === \"cached\");\n if (hasAnyLoaded && !this._hasLoadedThumbnails) {\n this._hasLoadedThumbnails = true;\n this.dispatchEvent(new CustomEvent(\"thumbnails-ready\", { bubbles: true }));\n }\n }\n }\n\n /**\n * Capture thumbnails from a timegroup target.\n */\n private async _captureTimegroupThumbnails(slots: ThumbnailSlot[]): Promise<void> {\n const target = this._targetElement as EFTimegroup;\n const { rootId, elementId } = getCacheIdentifiers(target);\n\n // Calculate capture scale\n const timegroupWidth = target.offsetWidth || 1920;\n const timegroupHeight = target.offsetHeight || 1080;\n const scale = Math.min(1, this._height / timegroupHeight, MAX_CAPTURE_WIDTH / timegroupWidth);\n\n // Process in batches\n for (let i = 0; i < slots.length; i += BATCH_SIZE) {\n const batch = slots.slice(i, i + BATCH_SIZE);\n const timestamps = batch.map((s) => s.timeMs);\n\n try {\n const canvases = await target.captureBatch(timestamps, {\n scale,\n contentReadyMode: \"immediate\",\n });\n\n for (let j = 0; j < batch.length; j++) {\n const slot = batch[j]!;\n const canvas = canvases[j];\n\n if (canvas) {\n const imageData = this._canvasToImageData(canvas);\n if (imageData) {\n const key = getCacheKey(rootId, elementId, slot.timeMs);\n sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);\n slot.imageData = imageData;\n slot.status = \"cached\";\n }\n }\n }\n\n // Redraw after each batch for progressive feedback\n this._drawCanvas();\n\n // Yield to main thread between batches\n if (i + BATCH_SIZE < slots.length) {\n await new Promise((r) => requestAnimationFrame(r));\n }\n } catch (error) {\n console.warn(\"Batch capture failed:\", error);\n }\n }\n }\n\n /**\n * Capture thumbnails from a video target using MediaEngine.\n */\n private async _captureVideoThumbnails(slots: ThumbnailSlot[]): Promise<void> {\n const target = this._targetElement as EFVideo;\n const { rootId, elementId } = getCacheIdentifiers(target);\n\n // Wait for media engine\n if (target.mediaEngineTask) {\n await target.mediaEngineTask.taskComplete;\n }\n\n const mediaEngine = target.mediaEngineTask?.value;\n if (!mediaEngine) return;\n\n // Check for video rendition\n const videoRendition = mediaEngine.getVideoRendition();\n const scrubRendition = mediaEngine.getScrubVideoRendition();\n if (!videoRendition && !scrubRendition) return;\n\n const timestamps = slots.map((s) => s.timeMs);\n\n // Create an abort controller for this thumbnail extraction\n // ThumbnailExtractor requires a signal to properly handle cleanup\n const abortController = new AbortController();\n\n try {\n const results = await mediaEngine.extractThumbnails(timestamps, abortController.signal);\n\n for (let i = 0; i < slots.length; i++) {\n const slot = slots[i]!;\n const result = results[i];\n\n if (result?.thumbnail) {\n const imageData = this._canvasToImageData(result.thumbnail);\n if (imageData) {\n const key = getCacheKey(rootId, elementId, slot.timeMs);\n sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);\n slot.imageData = imageData;\n slot.status = \"cached\";\n }\n }\n }\n } catch (error) {\n // Abort on error to clean up any in-flight requests\n abortController.abort();\n console.warn(\"Video thumbnail extraction failed:\", error);\n }\n }\n\n /**\n * Convert canvas to ImageData.\n */\n private _canvasToImageData(canvas: HTMLCanvasElement | OffscreenCanvas): ImageData | null {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true }) as\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | null;\n if (!ctx) return null;\n return ctx.getImageData(0, 0, canvas.width, canvas.height);\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public API\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Returns a promise that resolves when thumbnails are ready.\n * Resolves immediately if thumbnails are already loaded.\n */\n whenReady(): Promise<void> {\n if (this._hasLoadedThumbnails) {\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n this.addEventListener(\"thumbnails-ready\", () => resolve(), { once: true });\n });\n }\n\n /**\n * Check if thumbnails have been loaded.\n */\n get isReady(): boolean {\n return this._hasLoadedThumbnails;\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Cache Invalidation\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Invalidate cached thumbnails for this element within a time range.\n * Call this when content changes at specific times.\n */\n invalidateTimeRange(startTimeMs: number, endTimeMs: number): void {\n if (!this._targetElement) return;\n\n const { rootId, elementId } = getCacheIdentifiers(this._targetElement);\n sessionThumbnailCache.invalidateTimeRange(rootId, elementId, startTimeMs, endTimeMs);\n\n // Reset affected slots\n for (const slot of this._thumbnailSlots) {\n if (slot.timeMs >= startTimeMs && slot.timeMs <= endTimeMs) {\n slot.imageData = undefined;\n slot.status = \"pending\";\n }\n }\n\n this._scheduleRender();\n }\n\n /**\n * Invalidate all cached thumbnails for this element.\n */\n invalidateAll(): void {\n if (!this._targetElement) return;\n\n const { rootId, elementId } = getCacheIdentifiers(this._targetElement);\n sessionThumbnailCache.invalidateElement(rootId, elementId);\n\n // Reset all slots\n for (const slot of this._thumbnailSlots) {\n slot.imageData = undefined;\n slot.status = \"pending\";\n }\n\n this._scheduleRender();\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Render\n // ─────────────────────────────────────────────────────────────────────────\n\n render() {\n return html`<canvas ${ref(this.canvasRef)}></canvas>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-thumbnail-strip\": EFThumbnailStrip;\n }\n}\n\n// Re-export cache for backwards compatibility and debugging\nexport { sessionThumbnailCache as thumbnailImageCache } from \"./SessionThumbnailCache.js\";\n"],"mappings":";;;;;;;;;;;;AAYA,SAAS,UAAU,SAA6C;AAC9D,QAAO,SAAS,QAAQ,aAAa,KAAK;;;AAI5C,SAAS,cAAc,SAAiD;AACtE,QAAO,SAAS,QAAQ,aAAa,KAAK;;;;;;AAO5C,SAAS,oBAAoB,SAAuE;CAElG,MAAM,eAAe,iBAAiB,QAAQ;AAS9C,QAAO;EAAE,SARa,gBAAgB,cAAc,aAAa,GAAG,eAAe,OACrD,MAAM;EAOnB,WAJC,UAAU,QAAQ,GAChC,QAAQ,OAAO,QAAQ,MAAM,UAC7B,QAAQ,MAAM;EAEU;;;AAI9B,MAAM,4BAA4B;;AAGlC,MAAM,cAAc;;AAGpB,MAAM,uBAAuB,KAAK;;AAGlC,MAAM,oBAAoB;;AAG1B,MAAM,aAAa;AAWZ,6BAAMA,2BAAyB,WAAW;;;mBAuBnC,WAA8B;gBAOjC;wBAGQ;aAGX;8BAiBiB;qBAGT;2BAcgC,IAAI,iBAAiB,KAAY;wBAExB;gBA2BtC;iBACC;0BAG6B;4BAClB;0BAMF;yBAGgB,EAAE;4BAGhB;0BAYF;8BAGI;yBA8LC;AAC9B,OAAI,CAAC,KAAK,iBAAkB;AAC5B,QAAK,qBAAqB,KAAK,iBAAiB;AAChD,QAAK,aAAa;AAGlB,OAAI,CAAC,KAAK,aACR,MAAK,eAAe,4BAA4B;AAC9C,SAAK,eAAe;AACpB,SAAK,wBAAwB;KAC7B;;;;gBA1UU,CACd,GAAG;;;;;;;;;;;;;;;;;;MAmBJ;;CAqDD,IACI,gBAA8C;AAChD,SAAO,KAAK;;CAGd,IAAI,cAAc,OAAqC;EACrD,MAAM,WAAW,KAAK;AACtB,OAAK,iBAAiB;AAGtB,OAAK,mBAAmB,YAAY;AAGpC,MAAI,UAAU,SACZ,MAAK,uBAAuB;AAG9B,MAAI,SAAS,UAAU,SACrB,MAAK,qBAAqB,MAAM;AAGlC,OAAK,cAAc,iBAAiB,SAAS;;CA0C/C,oBAAoB;AAClB,QAAM,mBAAmB;AAGzB,OAAK,kBAAkB,IAAI,gBAAgB,YAAY;AACrD,QAAK,MAAM,SAAS,SAAS;IAC3B,MAAM,MAAM,MAAM,gBAAgB;AAClC,SAAK,SAAS,KAAK,cAAc,MAAM,YAAY;AACnD,SAAK,UAAU,KAAK,aAAa,MAAM,YAAY;AAEnD,SAAK,uBAAuB;AAC5B,SAAK,iBAAiB;;IAExB;AACF,OAAK,gBAAgB,QAAQ,KAAK;AAGlC,OAAK,eAAe,WAAW;AAC7B,QAAK,sBAAsB;AAC3B,QAAK,iBAAiB;IACtB;;CAGJ,uBAAuB;AACrB,QAAM,sBAAsB;AAC5B,OAAK,iBAAiB,YAAY;AAClC,OAAK,mBAAmB,YAAY;AACpC,OAAK,uBAAuB;AAE5B,MAAI,KAAK,aACP,sBAAqB,KAAK,aAAa;;CAI3C,QAAQ,mBAA2D;AACjE,QAAM,QAAQ,kBAAkB;AAGhC,MACE,kBAAkB,IAAI,iBAAiB,IACvC,kBAAkB,IAAI,MAAM,IAC5B,kBAAkB,IAAI,cAAc,IACpC,kBAAkB,IAAI,YAAY,IAClC,kBAAkB,IAAI,uBAAuB,IAC7C,kBAAkB,IAAI,cAAc,IACpC,kBAAkB,IAAI,gBAAgB,CAEtC,MAAK,iBAAiB;AAIxB,MAAI,kBAAkB,IAAI,iBAAiB,CACzC,MAAK,kBAAkB;;CAQ3B,AAAQ,uBAA6B;EAEnC,IAAIC,OAAoB,KAAK;AAE7B,SAAO,MAAM;AAEX,OAAI,gBAAgB,aAAa;IAC/B,MAAM,QAAQ,iBAAiB,KAAK;AACpC,QAAI,MAAM,cAAc,UAAU,MAAM,cAAc,UAAU;AAC9D,UAAK,mBAAmB;AACxB,UAAK,uBAAuB;AAC5B,UAAK,uBAAuB;AAC5B;;;AAKJ,OAAI,KAAK,WACP,QAAO,KAAK;YACH,gBAAgB,WAEzB,QAAO,KAAK;OAEZ;;;;;;;;;CAWN,AAAQ,wBAA8B;AACpC,MAAI,CAAC,KAAK,kBAAkB;AAC1B,QAAK,mBAAmB;AACxB;;EAIF,MAAM,cAAc,KAAK,kBAAkB;AAC3C,MAAI,aAAa;GACf,MAAM,aAAa,KAAK,0BAA0B,YAAY;AAC9D,OAAI,aAAa,GAAG;AAClB,SAAK,mBAAmB;AACxB;;;AAKJ,OAAK,mBAAmB;;;;;CAM1B,AAAQ,mBAAmC;EACzC,IAAIA,OAAoB;AAExB,SAAO,MAAM;AAEX,OAAI,gBAAgB,WAAW,KAAK,QAAQ,aAAa,KAAK,kBAC5D,QAAO;GAIT,MAAMC,aAA0B,KAAK;AACrC,OAAI,sBAAsB,WACxB,QAAO,WAAW;OAElB,QAAO;;AAIX,SAAO;;;;;;CAOT,AAAQ,0BAA0B,aAA8B;EAC9D,MAAM,aAAa,YAAY;AAC/B,MAAI,CAAC,WAAY,QAAO;EAExB,MAAM,WAAW,WAAW,cAAc,aAAa;AACvD,MAAI,CAAC,SAAU,QAAO;AAEtB,SAAO,SAAS,uBAAuB,CAAC;;;;;;CAO1C,AAAQ,4BAAoC;EAC1C,MAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OAAQ,QAAO;AAGpB,MAAI,UAAU,OAAO,CAGnB,SAFoB,OAAO,eAAe,MACtB,KAAK,gBAAgB,eAAe;AAK1D,SAAO;;CAGT,AAAQ,wBAA8B;AACpC,MAAI,CAAC,KAAK,iBAAkB;AAC5B,OAAK,iBAAiB,iBAAiB,UAAU,KAAK,WAAW,EAAE,SAAS,MAAM,CAAC;AACnF,OAAK,qBAAqB,KAAK,iBAAiB;;CAGlD,AAAQ,wBAA8B;AACpC,MAAI,KAAK,kBAAkB;AACzB,QAAK,iBAAiB,oBAAoB,UAAU,KAAK,UAAU;AACnE,QAAK,mBAAmB;;;CAkB5B,AAAQ,mBAAyB;AAC/B,MAAI,CAAC,KAAK,kBAAkB,KAAK,iBAAkB;AACnD,OAAK,qBAAqB,KAAK,eAAe;AAC9C,OAAK,aAAa;;CAGpB,IAAY,iBAAyB;AACnC,MAAI,KAAK,gBAAgB,cACvB,QAAO,KAAK,eAAe;AAE7B,MAAI,KAAK,iBAEP,QAAO,KAAK,iBAAiB,cAAc,KAAK;AAElD,SAAO,KAAK;;CAOd,AAAQ,qBAAqB,QAAqC;AAChE,MAAI,UAAU,OAAO,EAAE;AAErB,QAAK,oBAAoB,IAAI,uBAAuB,KAAK,iBAAiB,CAAC;AAC3E,QAAK,kBAAkB,QAAQ,QAAQ;IACrC,YAAY;IACZ,iBAAiB;KAAC;KAAa;KAAW;KAAY;KAAa;KAAM;IAC1E,CAAC;AAGF,UAAO,eAAe,WAAW;AAC/B,QAAI,KAAK,mBAAmB,OAAQ;AACpC,WAAO,iBAAiB,aAAa,WAAW;AAC9C,SAAI,KAAK,mBAAmB,OAAQ;AACpC,UAAK,iBAAiB;MACtB;KACF;aACO,cAAc,OAAO,EAAE;AAEhC,QAAK,oBAAoB,IAAI,uBAAuB,KAAK,iBAAiB,CAAC;AAC3E,QAAK,kBAAkB,QAAQ,QAAQ;IACrC,WAAW;IACX,SAAS;IACV,CAAC;AAGF,OAAI,OAAO,eAAe,GAAG;IAC3B,MAAM,sBAAsB;AAC1B,SAAI,KAAK,mBAAmB,OAAQ;AACpC,SAAI,OAAO,aAAa,EACtB,MAAK,iBAAiB;SAEtB,uBAAsB,cAAc;;AAGxC,0BAAsB,cAAc;;;;CAS1C,AAAQ,kBAAwB;AAC9B,MAAI,KAAK,iBAAkB;AAC3B,OAAK,mBAAmB;AAExB,8BAA4B;AAC1B,QAAK,mBAAmB;AACxB,QAAK,kBAAkB;AACvB,QAAK,aAAa;AAClB,QAAK,aAAa;AAClB,QAAK,wBAAwB;AAI7B,QAAK,wBAAwB;IAC7B;;;;;CAMJ,AAAQ,yBAA+B;AACrC,MAAI,KAAK,qBAAsB;EAK/B,MAAM,YAAY,KAAK,gBAAgB,SAAS;EAChD,MAAM,eAAe,KAAK,gBAAgB,MAAM,MAAM,EAAE,WAAW,SAAS;EAC5E,MAAM,aAAa,KAAK,gBAAgB,MAAM,MAAM,EAAE,WAAW,UAAU;AAE3E,MAAI,cAAc,gBAAgB,CAAC,aAAa;AAC9C,QAAK,uBAAuB;AAC5B,QAAK,cAAc,IAAI,YAAY,oBAAoB,EAAE,SAAS,MAAM,CAAC,CAAC;;;;;;CAO9E,AAAQ,mBAAyB;AAC/B,MAAI,KAAK,UAAU,KAAK,KAAK,WAAW,KAAK,CAAC,KAAK,gBAAgB;AACjE,QAAK,kBAAkB,EAAE;AACzB;;EAGF,MAAM,YAAY,KAAK,eAAe;AACtC,MAAI,UAAU,SAAS,UAAU,SAAS;AACxC,QAAK,kBAAkB,EAAE;AACzB;;EAIF,MAAM,aAAa,KAAK,6BAA6B;EACrD,MAAM,MAAM,KAAK;EAGjB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,SAAS,QAAQ,aAAa,KAAK,CAAC;EAG/E,MAAM,QAAQ,QAAQ,KAAK,KAAK,SAAS,eAAe,QAAQ,KAAK;EAGrE,MAAMC,QAAyB,EAAE;EACjC,MAAM,WAAW,UAAU,QAAQ,UAAU;AAE7C,OAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAC9B,MAAM,SAAS,UAAU,KACpB,UAAU,UAAU,UAAU,SAAS,IACxC,UAAU,UAAW,IAAI,YAAa,QAAQ;AAElD,SAAM,KAAK;IACT;IACA,GAAG,KAAK,MAAM,IAAI,MAAM;IACxB,OAAO;IACP,QAAQ;IACT,CAAC;;AAGJ,OAAK,kBAAkB;;;;;CAMzB,AAAQ,gBAAoD;EAC1D,MAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OAAQ,QAAO;GAAE,SAAS;GAAG,OAAO;GAAG;AAE5C,MAAI,UAAU,OAAO,EAAE;AACrB,OAAI,KAAK,qBAEP,QAAO;IACL,SAAS,KAAK,eAAe;IAC7B,OAAO,KAAK,aAAa,OAAO,uBAAuB;IACxD;GAGH,MAAM,cAAc,OAAO,iBAAiB;GAC5C,MAAM,kBAAkB,OAAO,cAAc;AAC7C,UAAO;IACL,SAAS,KAAK,gBAAgB,SAAY,cAAc,KAAK,cAAc;IAC3E,OAAO,KAAK,cAAc,SAAY,cAAc,KAAK,YAAY,cAAc;IACpF;;AAIH,SAAO;GACL,SAAS,KAAK,eAAe;GAC7B,OAAQ,KAAK,aAAa,KAAK,YAAY,IAAK,KAAK,YAAY,OAAO,cAAc;GACvF;;;;;CAMH,AAAQ,8BAAsC;AAC5C,MAAI,KAAK,iBAAiB,EAAG,QAAO,KAAK;EAEzC,MAAM,SAAS,KAAK;EACpB,IAAI,cAAc;AAElB,MAAI,UAAU,OAAO,CAGnB,gBAFW,OAAe,cAAc,SAC7B,OAAe,eAAe;WAEhC,cAAc,OAAO,CAG9B,gBAFU,OAAO,eAAe,SACtB,OAAO,gBAAgB;AAInC,SAAO,KAAK,MAAM,KAAK,UAAU,YAAY;;;;;CAM/C,AAAQ,cAAoB;AAC1B,MAAI,CAAC,KAAK,eAAgB;EAE1B,MAAM,EAAE,QAAQ,cAAc,oBAAoB,KAAK,eAAe;AAEtE,OAAK,MAAM,QAAQ,KAAK,iBAAiB;GACvC,MAAM,MAAM,YAAY,QAAQ,WAAW,KAAK,OAAO;AACvD,OAAI,sBAAsB,IAAI,IAAI,EAAE;AAClC,SAAK,YAAY,sBAAsB,IAAI,IAAI;AAC/C,SAAK,SAAS;;;;;;;;;CAUpB,AAAQ,cAAoB;EAC1B,MAAM,SAAS,KAAK,UAAU;AAC9B,MAAI,CAAC,OAAQ;EAEb,MAAM,MAAM,OAAO,WAAW,MAAM,EAAE,oBAAoB,MAAM,CAAC;AACjE,MAAI,CAAC,IAAK;EAEV,MAAM,aAAa,KAAK;EACxB,MAAM,SAAS,KAAK;AAEpB,MAAI,cAAc,KAAK,UAAU,EAAG;EAEpC,MAAM,MAAM,OAAO,oBAAoB;EAGvC,MAAM,aAAa,KAAK;EACxB,MAAM,gBAAgB,KAAK;EAG3B,MAAM,eAAe,KAAK,2BAA2B;EACrD,MAAM,aAAa,eAAe;EAGlC,MAAM,gBAAgB,aAAa;EACnC,MAAM,iBAAiB,aAAa,gBAAgB;AAGpD,MAAI,aAAa,iBAAiB,eAAe,gBAAgB;AAC/D,UAAO,MAAM,UAAU;AACvB;;AAEF,SAAO,MAAM,UAAU;EAIvB,MAAM,sBAAsB,KAAK,IAAI,GAAG,gBAAgB,aAAa;EACrE,MAAM,oBAAoB,KAAK,IAAI,YAAY,iBAAiB,aAAa;EAC7E,MAAM,iBAAiB,oBAAoB;AAE3C,MAAI,kBAAkB,GAAG;AACvB,UAAO,MAAM,UAAU;AACvB;;EAIF,MAAM,cAAc,KAAK,KAAK,iBAAiB,IAAI;EACnD,MAAM,eAAe,KAAK,KAAK,SAAS,IAAI;AAE5C,MAAI,OAAO,UAAU,eAAe,OAAO,WAAW,cAAc;AAClE,UAAO,QAAQ;AACf,UAAO,SAAS;;AAIlB,SAAO,MAAM,OAAO,GAAG,oBAAoB;AAC3C,SAAO,MAAM,QAAQ,GAAG,eAAe;AACvC,SAAO,MAAM,SAAS,GAAG,OAAO;AAGhC,MAAI,aAAa,KAAK,GAAG,GAAG,KAAK,GAAG,EAAE;AAGtC,MAAI,YAAY;AAChB,MAAI,SAAS,GAAG,GAAG,gBAAgB,OAAO;AAG1C,OAAK,MAAM,QAAQ,KAAK,iBAAiB;AAIvC,OAHkB,KAAK,IAAI,KAAK,QAGhB,uBAAuB,KAAK,IAAI,kBAAmB;GAGnE,MAAM,QAAQ,KAAK,IAAI;AAGvB,OAAI,QAAQ,KAAK,QAAQ,KAAK,QAAQ,eAAgB;AAEtD,OAAI,KAAK,UACP,MAAK,oBAAoB,KAAK,KAAK,WAAW,OAAO,KAAK,OAAO,OAAO;QACnE;AAEL,QAAI,YAAY,KAAK,WAAW,YAAY,YAAY;AACxD,QAAI,SAAS,OAAO,GAAG,KAAK,OAAO,OAAO;AAG1C,QAAI,KAAK,WAAW,WAAW;AAC7B,SAAI,YAAY;AAChB,SAAI,SAAS,OAAO,GAAG,KAAK,OAAO,EAAE;;;;;;;;CAS7C,AAAQ,oBACN,KACA,WACA,GACA,OACA,QACM;EAEN,MAAM,aAAa,SAAS,cAAc,SAAS;AACnD,aAAW,QAAQ,UAAU;AAC7B,aAAW,SAAS,UAAU;EAC9B,MAAM,UAAU,WAAW,WAAW,KAAK;AAC3C,MAAI,CAAC,QAAS;AACd,UAAQ,aAAa,WAAW,GAAG,EAAE;EAGrC,MAAM,YAAY,UAAU,QAAQ,UAAU;EAC9C,MAAM,YAAY,QAAQ;EAE1B,IAAI,OAAO,GAAG,OAAO,GAAG,OAAO,UAAU,OAAO,OAAO,UAAU;AAEjE,MAAI,YAAY,WAAW;AACzB,UAAO,UAAU,SAAS;AAC1B,WAAQ,UAAU,QAAQ,QAAQ;SAC7B;AACL,UAAO,UAAU,QAAQ;AACzB,WAAQ,UAAU,SAAS,QAAQ;;AAGrC,MAAI,UAAU,YAAY,MAAM,MAAM,MAAM,MAAM,GAAG,GAAG,OAAO,OAAO;;;;;CAUxE,MAAc,yBAAwC;AACpD,MAAI,KAAK,sBAAsB,CAAC,KAAK,eAAgB;EAErD,MAAM,gBAAgB,KAAK;EAC3B,MAAM,eAAe,KAAK;EAC1B,MAAM,aAAa,KAAK;EAGxB,MAAM,eAAe,KAAK,2BAA2B;EAGrD,MAAM,gBAAgB,eAAe;EACrC,MAAM,iBAAiB,eAAe,gBAAgB;EAGtD,MAAM,sBAAsB,KAAK,IAAI,GAAG,gBAAgB,aAAa;EACrE,MAAM,oBAAoB,KAAK,IAAI,YAAY,iBAAiB,aAAa;EAG7E,MAAM,UAAU,KAAK,gBAAgB,QAAQ,SAAS;AACpD,OAAI,KAAK,WAAW,UAAW,QAAO;AAEtC,UADkB,KAAK,IAAI,KAAK,SACZ,uBAAuB,KAAK,KAAK;IACrD;AAEF,MAAI,QAAQ,WAAW,EAAG;AAE1B,OAAK,qBAAqB;AAG1B,OAAK,MAAM,QAAQ,QACjB,MAAK,SAAS;AAEhB,OAAK,aAAa;AAElB,MAAI;AACF,OAAI,cAAc,KAAK,eAAe,CACpC,OAAM,KAAK,4BAA4B,QAAQ;YACtC,UAAU,KAAK,eAAe,CACvC,OAAM,KAAK,wBAAwB,QAAQ;WAEtC,OAAO;AACd,WAAQ,KAAK,iCAAiC,MAAM;AAEpD,QAAK,MAAM,QAAQ,QACjB,KAAI,KAAK,WAAW,UAClB,MAAK,SAAS;YAGV;AACR,QAAK,qBAAqB;AAC1B,QAAK,aAAa;AAIlB,OADqB,KAAK,gBAAgB,MAAM,MAAM,EAAE,WAAW,SAAS,IACxD,CAAC,KAAK,sBAAsB;AAC9C,SAAK,uBAAuB;AAC5B,SAAK,cAAc,IAAI,YAAY,oBAAoB,EAAE,SAAS,MAAM,CAAC,CAAC;;;;;;;CAQhF,MAAc,4BAA4B,OAAuC;EAC/E,MAAM,SAAS,KAAK;EACpB,MAAM,EAAE,QAAQ,cAAc,oBAAoB,OAAO;EAGzD,MAAM,iBAAiB,OAAO,eAAe;EAC7C,MAAM,kBAAkB,OAAO,gBAAgB;EAC/C,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,UAAU,iBAAiB,oBAAoB,eAAe;AAG7F,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,YAAY;GACjD,MAAM,QAAQ,MAAM,MAAM,GAAG,IAAI,WAAW;GAC5C,MAAM,aAAa,MAAM,KAAK,MAAM,EAAE,OAAO;AAE7C,OAAI;IACF,MAAM,WAAW,MAAM,OAAO,aAAa,YAAY;KACrD;KACA,kBAAkB;KACnB,CAAC;AAEF,SAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;KACrC,MAAM,OAAO,MAAM;KACnB,MAAM,SAAS,SAAS;AAExB,SAAI,QAAQ;MACV,MAAM,YAAY,KAAK,mBAAmB,OAAO;AACjD,UAAI,WAAW;OACb,MAAM,MAAM,YAAY,QAAQ,WAAW,KAAK,OAAO;AACvD,6BAAsB,IAAI,KAAK,WAAW,KAAK,QAAQ,UAAU;AACjE,YAAK,YAAY;AACjB,YAAK,SAAS;;;;AAMpB,SAAK,aAAa;AAGlB,QAAI,IAAI,aAAa,MAAM,OACzB,OAAM,IAAI,SAAS,MAAM,sBAAsB,EAAE,CAAC;YAE7C,OAAO;AACd,YAAQ,KAAK,yBAAyB,MAAM;;;;;;;CAQlD,MAAc,wBAAwB,OAAuC;EAC3E,MAAM,SAAS,KAAK;EACpB,MAAM,EAAE,QAAQ,cAAc,oBAAoB,OAAO;AAGzD,MAAI,OAAO,gBACT,OAAM,OAAO,gBAAgB;EAG/B,MAAM,cAAc,OAAO,iBAAiB;AAC5C,MAAI,CAAC,YAAa;EAGlB,MAAM,iBAAiB,YAAY,mBAAmB;EACtD,MAAM,iBAAiB,YAAY,wBAAwB;AAC3D,MAAI,CAAC,kBAAkB,CAAC,eAAgB;EAExC,MAAM,aAAa,MAAM,KAAK,MAAM,EAAE,OAAO;EAI7C,MAAM,kBAAkB,IAAI,iBAAiB;AAE7C,MAAI;GACF,MAAM,UAAU,MAAM,YAAY,kBAAkB,YAAY,gBAAgB,OAAO;AAEvF,QAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;IACrC,MAAM,OAAO,MAAM;IACnB,MAAM,SAAS,QAAQ;AAEvB,QAAI,QAAQ,WAAW;KACrB,MAAM,YAAY,KAAK,mBAAmB,OAAO,UAAU;AAC3D,SAAI,WAAW;MACb,MAAM,MAAM,YAAY,QAAQ,WAAW,KAAK,OAAO;AACvD,4BAAsB,IAAI,KAAK,WAAW,KAAK,QAAQ,UAAU;AACjE,WAAK,YAAY;AACjB,WAAK,SAAS;;;;WAIb,OAAO;AAEd,mBAAgB,OAAO;AACvB,WAAQ,KAAK,sCAAsC,MAAM;;;;;;CAO7D,AAAQ,mBAAmB,QAA+D;EACxF,MAAM,MAAM,OAAO,WAAW,MAAM,EAAE,oBAAoB,MAAM,CAAC;AAIjE,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,IAAI,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,OAAO;;;;;;CAW5D,YAA2B;AACzB,MAAI,KAAK,qBACP,QAAO,QAAQ,SAAS;AAE1B,SAAO,IAAI,SAAS,YAAY;AAC9B,QAAK,iBAAiB,0BAA0B,SAAS,EAAE,EAAE,MAAM,MAAM,CAAC;IAC1E;;;;;CAMJ,IAAI,UAAmB;AACrB,SAAO,KAAK;;;;;;CAWd,oBAAoB,aAAqB,WAAyB;AAChE,MAAI,CAAC,KAAK,eAAgB;EAE1B,MAAM,EAAE,QAAQ,cAAc,oBAAoB,KAAK,eAAe;AACtE,wBAAsB,oBAAoB,QAAQ,WAAW,aAAa,UAAU;AAGpF,OAAK,MAAM,QAAQ,KAAK,gBACtB,KAAI,KAAK,UAAU,eAAe,KAAK,UAAU,WAAW;AAC1D,QAAK,YAAY;AACjB,QAAK,SAAS;;AAIlB,OAAK,iBAAiB;;;;;CAMxB,gBAAsB;AACpB,MAAI,CAAC,KAAK,eAAgB;EAE1B,MAAM,EAAE,QAAQ,cAAc,oBAAoB,KAAK,eAAe;AACtE,wBAAsB,kBAAkB,QAAQ,UAAU;AAG1D,OAAK,MAAM,QAAQ,KAAK,iBAAiB;AACvC,QAAK,YAAY;AACjB,QAAK,SAAS;;AAGhB,OAAK,iBAAiB;;CAOxB,SAAS;AACP,SAAO,IAAI,WAAW,IAAI,KAAK,UAAU,CAAC;;;YA94B3C,SAAS,EAAE,MAAM,QAAQ,CAAC;YAG1B,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAmB,CAAC;YAGxD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAiB,CAAC;YAGtD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAe,CAAC;YAGpD,SAAS;CACR,MAAM;CACN,WAAW;CACX,SAAS;CACT,WAAW;EACT,gBAAgB,UAAyB,UAAU;EACnD,cAAc,UAAoB,QAAQ,SAAS;EACpD;CACF,CAAC;YAGD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAiB,CAAC;YAQtD,QAAQ;CAAE,SAAS;CAAsB,WAAW;CAAM,CAAC,EAC3D,OAAO;YAUP,OAAO;+BA3ET,cAAc,qBAAqB"}
|
|
1
|
+
{"version":3,"file":"EFThumbnailStrip.js","names":["EFThumbnailStrip","node: Node | null","parentNode: Node | null","slots: ThumbnailSlot[]"],"sources":["../../src/elements/EFThumbnailStrip.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport type { EFVideo } from \"./EFVideo.js\";\nimport type { EFTimegroup } from \"./EFTimegroup.js\";\nimport { TargetController } from \"./TargetController.ts\";\nimport { timelineStateContext, type TimelineState } from \"../gui/timeline/timelineStateContext.js\";\nimport { sessionThumbnailCache, getCacheKey } from \"./SessionThumbnailCache.js\";\nimport { findRootTemporal } from \"./findRootTemporal.js\";\n\n/** Type guard to check if element is EFVideo */\nfunction isEFVideo(element: Element | null): element is EFVideo {\n return element?.tagName.toLowerCase() === \"ef-video\";\n}\n\n/** Type guard to check if element is EFTimegroup */\nfunction isEFTimegroup(element: Element | null): element is EFTimegroup {\n return element?.tagName.toLowerCase() === \"ef-timegroup\";\n}\n\n/**\n * Get identifiers for cache key generation.\n * Returns rootId (for cache isolation), elementId (for element-specific caching), and epoch (for content versioning).\n */\nfunction getCacheIdentifiers(element: EFVideo | EFTimegroup): { rootId: string; elementId: string; epoch: number } {\n // Get root timegroup for cache isolation between projects\n const rootTemporal = findRootTemporal(element);\n const rootTimegroup = rootTemporal && isEFTimegroup(rootTemporal) ? rootTemporal : null;\n const rootId = rootTimegroup?.id || \"default\";\n const epoch = rootTimegroup?.contentEpoch ?? 0;\n\n // Element identifier\n const elementId = isEFVideo(element)\n ? element.src || element.id || \"video\"\n : element.id || \"timegroup\";\n\n return { rootId, elementId, epoch };\n}\n\n/** Padding in pixels for virtual rendering (render extra thumbnails beyond viewport) */\nconst VIRTUAL_RENDER_PADDING_PX = 200;\n\n/** Default gap between thumbnails */\nconst DEFAULT_GAP = 4;\n\n/** Default aspect ratio if unknown */\nconst DEFAULT_ASPECT_RATIO = 16 / 9;\n\n/** Max canvas width for thumbnail captures */\nconst MAX_CAPTURE_WIDTH = 480;\n\n/** Thumbnails to capture per batch */\nconst BATCH_SIZE = 10;\n\ninterface ThumbnailSlot {\n timeMs: number;\n x: number; // Absolute position (not scroll-adjusted)\n width: number;\n imageData?: ImageData;\n status: \"cached\" | \"loading\" | \"pending\";\n}\n\n@customElement(\"ef-thumbnail-strip\")\nexport class EFThumbnailStrip extends LitElement {\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n background: #1a1a2e;\n overflow: hidden;\n width: 100%;\n height: 100%;\n }\n canvas {\n display: block;\n /* Absolute positioning - we manually position at visible region */\n position: absolute;\n top: 0;\n /* Left and width set programmatically based on visible portion */\n height: 100%;\n image-rendering: auto;\n }\n `,\n ];\n\n canvasRef = createRef<HTMLCanvasElement>();\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public Properties\n // ─────────────────────────────────────────────────────────────────────────\n\n @property({ type: String })\n target = \"\";\n\n @property({ type: Number, attribute: \"thumbnail-width\" })\n thumbnailWidth = 0; // 0 = auto (calculate from height using aspect ratio)\n\n @property({ type: Number, attribute: \"gap\" })\n gap = DEFAULT_GAP;\n\n @property({ type: Number, attribute: \"start-time-ms\" })\n startTimeMs?: number;\n\n @property({ type: Number, attribute: \"end-time-ms\" })\n endTimeMs?: number;\n\n @property({\n type: Boolean,\n attribute: \"use-intrinsic-duration\",\n reflect: true,\n converter: {\n fromAttribute: (value: string | null) => value === \"true\",\n toAttribute: (value: boolean) => (value ? \"true\" : null),\n },\n })\n useIntrinsicDuration = false;\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs = 0.1;\n\n // ─────────────────────────────────────────────────────────────────────────\n // Internal State\n // ─────────────────────────────────────────────────────────────────────────\n\n /** Timeline state context for viewport info */\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n private _timelineState?: TimelineState;\n\n /** Target element controller */\n // @ts-expect-error controller used for side effects\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for side effects\n private _targetController: TargetController = new TargetController(this as any);\n\n private _targetElement: EFVideo | EFTimegroup | null = null;\n\n @state()\n get targetElement(): EFVideo | EFTimegroup | null {\n return this._targetElement;\n }\n\n set targetElement(value: EFVideo | EFTimegroup | null) {\n const oldValue = this._targetElement;\n this._targetElement = value;\n\n // Clean up old observer\n this._mutationObserver?.disconnect();\n\n // Reset ready state when target changes\n if (value !== oldValue) {\n this._hasLoadedThumbnails = false;\n this._lastLoadedEpoch = null;\n this._lastLayoutParams = null;\n }\n\n if (value && value !== oldValue) {\n this._setupTargetObserver(value);\n }\n\n this.requestUpdate(\"targetElement\", oldValue);\n }\n\n /** Host element dimensions */\n private _width = 0;\n private _height = 0;\n\n /** Scroll container reference */\n private _scrollContainer: HTMLElement | null = null;\n private _currentScrollLeft = 0;\n \n /** \n * Offset from scroll container's left edge to this element's track.\n * Used for sticky positioning when track doesn't start at x=0 (e.g., labels column).\n */\n private _trackLeftOffset = 0;\n\n /** Current thumbnail slots */\n private _thumbnailSlots: ThumbnailSlot[] = [];\n\n /** Capture in progress flag */\n private _captureInProgress = false;\n\n /** Resize observer */\n private _resizeObserver?: ResizeObserver;\n\n /** Mutation observer for target element changes */\n private _mutationObserver?: MutationObserver;\n\n /** Animation frame for scroll updates */\n private _scrollFrame?: number;\n\n /** Render request tracking */\n private _renderRequested = false;\n\n /** Track if any thumbnails have been loaded (for ready event) */\n private _hasLoadedThumbnails = false;\n\n /** Track the last epoch we loaded thumbnails for */\n private _lastLoadedEpoch: number | null = null;\n \n /** Track layout parameters to avoid unnecessary slot recreation */\n private _lastLayoutParams: {\n width: number;\n height: number;\n startTimeMs: number;\n endTimeMs: number;\n thumbWidth: number;\n gap: number;\n } | null = null;\n\n // ─────────────────────────────────────────────────────────────────────────\n // Lifecycle\n // ─────────────────────────────────────────────────────────────────────────\n\n connectedCallback() {\n super.connectedCallback();\n\n // Set up resize observer\n this._resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const box = entry.borderBoxSize?.[0];\n this._width = box?.inlineSize ?? entry.contentRect.width;\n this._height = box?.blockSize ?? entry.contentRect.height;\n // Recalculate track offset in case layout changed\n this._calculateTrackOffset();\n this._scheduleRender();\n }\n });\n this._resizeObserver.observe(this);\n\n // Find scroll container after element is ready\n this.updateComplete.then(() => {\n this._findScrollContainer();\n this._scheduleRender();\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this._resizeObserver?.disconnect();\n this._mutationObserver?.disconnect();\n this._detachScrollListener();\n\n if (this._scrollFrame) {\n cancelAnimationFrame(this._scrollFrame);\n }\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated(changedProperties);\n\n // Re-render on property changes\n if (\n changedProperties.has(\"thumbnailWidth\") ||\n changedProperties.has(\"gap\") ||\n changedProperties.has(\"startTimeMs\") ||\n changedProperties.has(\"endTimeMs\") ||\n changedProperties.has(\"useIntrinsicDuration\") ||\n changedProperties.has(\"pixelsPerMs\") ||\n changedProperties.has(\"targetElement\")\n ) {\n this._scheduleRender();\n }\n\n // Handle timeline context scroll changes\n if (changedProperties.has(\"_timelineState\")) {\n this._onContextScroll();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Scroll Handling\n // ─────────────────────────────────────────────────────────────────────────\n\n private _findScrollContainer(): void {\n // Walk up the DOM tree, crossing shadow boundaries\n let node: Node | null = this.parentNode;\n \n while (node) {\n // Check if this node is an element with overflow-x: auto or scroll\n if (node instanceof HTMLElement) {\n const style = getComputedStyle(node);\n if (style.overflowX === \"auto\" || style.overflowX === \"scroll\") {\n this._scrollContainer = node;\n this._calculateTrackOffset();\n this._attachScrollListener();\n return;\n }\n }\n \n // Move to parent, crossing shadow DOM boundaries\n if (node.parentNode) {\n node = node.parentNode;\n } else if (node instanceof ShadowRoot) {\n // Cross shadow boundary to the host element\n node = node.host;\n } else {\n break;\n }\n }\n }\n\n /**\n * Calculate the horizontal offset from scroll container's left edge to this element's track.\n * This accounts for sticky labels or other elements that precede the track area.\n * \n * We look for our specific timeline elements (ef-timeline-row) and measure their label width.\n */\n private _calculateTrackOffset(): void {\n if (!this._scrollContainer) {\n this._trackLeftOffset = 0;\n return;\n }\n \n // Find ef-timeline-row ancestor and get its label width\n const timelineRow = this._findTimelineRow();\n if (timelineRow) {\n const labelWidth = this._getTimelineRowLabelWidth(timelineRow);\n if (labelWidth > 0) {\n this._trackLeftOffset = labelWidth;\n return;\n }\n }\n \n // No timeline row found - track starts at scroll container's left edge\n this._trackLeftOffset = 0;\n }\n \n /**\n * Find the ef-timeline-row ancestor by walking up through shadow DOM boundaries.\n */\n private _findTimelineRow(): Element | null {\n let node: Node | null = this;\n \n while (node) {\n // Check if this is ef-timeline-row\n if (node instanceof Element && node.tagName.toLowerCase() === 'ef-timeline-row') {\n return node;\n }\n \n // Move up through shadow DOM boundaries\n const parentNode: Node | null = node.parentNode;\n if (parentNode instanceof ShadowRoot) {\n node = parentNode.host;\n } else {\n node = parentNode;\n }\n }\n \n return null;\n }\n \n /**\n * Get the label width from an ef-timeline-row element.\n * Queries the shadow root for .row-label and returns its width.\n */\n private _getTimelineRowLabelWidth(timelineRow: Element): number {\n const shadowRoot = timelineRow.shadowRoot;\n if (!shadowRoot) return 0;\n \n const rowLabel = shadowRoot.querySelector('.row-label');\n if (!rowLabel) return 0;\n \n return rowLabel.getBoundingClientRect().width;\n }\n \n /**\n * Get this strip's absolute position in the timeline (pixels from timeline origin).\n * Uses the target element's startTimeMs to determine position.\n */\n private _getStripTimelinePosition(): number {\n const target = this._targetElement;\n if (!target) return 0;\n \n // For videos, use their startTimeMs to get timeline position\n if (isEFVideo(target)) {\n const startTimeMs = target.startTimeMs ?? 0;\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? 0.1;\n return startTimeMs * pixelsPerMs;\n }\n \n // For root timegroup, position is 0\n return 0;\n }\n\n private _attachScrollListener(): void {\n if (!this._scrollContainer) return;\n this._scrollContainer.addEventListener(\"scroll\", this._onScroll, { passive: true });\n this._currentScrollLeft = this._scrollContainer.scrollLeft;\n }\n\n private _detachScrollListener(): void {\n if (this._scrollContainer) {\n this._scrollContainer.removeEventListener(\"scroll\", this._onScroll);\n this._scrollContainer = null;\n }\n }\n\n private _onScroll = (): void => {\n if (!this._scrollContainer) return;\n this._currentScrollLeft = this._scrollContainer.scrollLeft;\n this._drawCanvas();\n\n // Schedule loading of newly visible thumbnails\n if (!this._scrollFrame) {\n this._scrollFrame = requestAnimationFrame(() => {\n this._scrollFrame = undefined;\n this._loadVisibleThumbnails();\n });\n }\n };\n\n private _onContextScroll(): void {\n if (!this._timelineState || this._scrollContainer) return;\n this._currentScrollLeft = this._timelineState.viewportScrollLeft;\n this._drawCanvas();\n }\n\n private get _viewportWidth(): number {\n if (this._timelineState?.viewportWidth) {\n return this._timelineState.viewportWidth;\n }\n if (this._scrollContainer) {\n // Subtract track offset to get actual viewport width available for the track\n return this._scrollContainer.clientWidth - this._trackLeftOffset;\n }\n return this._width;\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Target Observer\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Watch for async content loading from child media elements.\n * When media finishes loading, increment the epoch to invalidate cached thumbnails.\n */\n private _watchChildContentLoading(target: EFTimegroup): void {\n const mediaElements = target.querySelectorAll('ef-video, ef-image, ef-audio');\n \n for (const el of mediaElements) {\n // Watch EFVideo/EFAudio mediaEngineTask completion\n const mediaEngine = (el as any).mediaEngineTask;\n if (mediaEngine?.taskComplete) {\n mediaEngine.taskComplete.then(() => {\n if (this._targetElement === target) {\n target.incrementContentEpoch();\n // Reset layout params to force slot recreation with new epoch\n this._lastLayoutParams = null;\n this._scheduleRender();\n }\n }).catch(() => {\n // Ignore abort errors\n });\n }\n \n // Watch EFImage fetchImage completion \n const fetchTask = (el as any).fetchImage;\n if (fetchTask?.taskComplete) {\n fetchTask.taskComplete.then(() => {\n if (this._targetElement === target) {\n target.incrementContentEpoch();\n // Reset layout params to force slot recreation with new epoch\n this._lastLayoutParams = null;\n this._scheduleRender();\n }\n }).catch(() => {\n // Ignore abort errors\n });\n }\n }\n }\n\n private _setupTargetObserver(target: EFVideo | EFTimegroup): void {\n if (isEFVideo(target)) {\n // Watch video property changes\n this._mutationObserver = new MutationObserver(() => this._scheduleRender());\n this._mutationObserver.observe(target, {\n attributes: true,\n attributeFilter: [\"trimstart\", \"trimend\", \"sourcein\", \"sourceout\", \"src\"],\n });\n\n // Wait for media engine\n target.updateComplete.then(() => {\n if (this._targetElement !== target) return;\n target.mediaEngineTask?.taskComplete.then(() => {\n if (this._targetElement !== target) return;\n this._scheduleRender();\n });\n });\n } else if (isEFTimegroup(target)) {\n // Watch timegroup structure and content changes\n this._mutationObserver = new MutationObserver((mutations) => {\n // Double-check that mutations are actually content-changing\n // (defensive check in case attributeFilter doesn't catch everything)\n const hasContentChange = mutations.some((mutation) => {\n if (mutation.type === \"childList\") {\n return mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0;\n }\n if (mutation.type === \"attributes\") {\n const attrName = mutation.attributeName;\n // Skip time/playback attributes that might slip through\n if (attrName === \"currenttime\" || attrName === \"current-time\" || \n attrName === \"playing\" || attrName === \"loop\") {\n return false;\n }\n // Only count visual content attributes\n return attrName === \"src\" || attrName === \"asset-id\" || \n attrName === \"style\" || attrName === \"transform\";\n }\n return false;\n });\n \n // Only increment epoch and schedule render if content actually changed\n if (hasContentChange) {\n const epochBefore = target.contentEpoch;\n target.incrementContentEpoch();\n const epochAfter = target.contentEpoch;\n \n // Only schedule render if epoch actually changed\n // (defensive check in case incrementContentEpoch was called elsewhere)\n if (epochAfter !== epochBefore) {\n // Reset layout params to force slot recreation with new epoch\n this._lastLayoutParams = null;\n \n // Check if new children were added\n const hasNewChildren = mutations.some(m => m.addedNodes.length > 0);\n \n // Re-watch content loading for new children\n if (hasNewChildren) {\n this._watchChildContentLoading(target);\n }\n \n this._scheduleRender();\n }\n }\n });\n this._mutationObserver.observe(target, {\n childList: true,\n subtree: true,\n attributes: true,\n // Only watch attributes that affect visual content\n // Exclude time/playback attributes (currenttime, playing, loop) and trim/source attributes\n // (those affect which part of content is shown, not the content itself)\n attributeFilter: [\"src\", \"asset-id\", \"style\", \"transform\"],\n });\n\n // Watch for async content loading from child media elements\n this._watchChildContentLoading(target);\n\n // Watch for duration becoming available\n if (target.durationMs === 0) {\n const checkDuration = () => {\n if (this._targetElement !== target) return;\n if (target.durationMs > 0) {\n this._scheduleRender();\n } else {\n requestAnimationFrame(checkDuration);\n }\n };\n requestAnimationFrame(checkDuration);\n }\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Rendering Pipeline\n // ─────────────────────────────────────────────────────────────────────────\n\n private _scheduleRender(): void {\n if (this._renderRequested) return;\n this._renderRequested = true;\n\n requestAnimationFrame(() => {\n this._renderRequested = false;\n \n this._calculateLayout();\n this._checkCache();\n this._drawCanvas();\n \n // Load any pending thumbnails (has its own epoch and state checks)\n this._loadVisibleThumbnails();\n\n // Check if we should dispatch ready event\n // (e.g., all thumbnails were already cached, or nothing to load)\n this._checkAndDispatchReady();\n });\n }\n\n /**\n * Check if thumbnails are ready and dispatch event if not already done.\n */\n private _checkAndDispatchReady(): void {\n if (this._hasLoadedThumbnails) return;\n\n // Consider ready if we have layout and either:\n // 1. Have some cached thumbnails, or\n // 2. Have no pending thumbnails (nothing to load)\n const hasLayout = this._thumbnailSlots.length > 0;\n const hasAnyCached = this._thumbnailSlots.some((s) => s.status === \"cached\");\n const hasPending = this._thumbnailSlots.some((s) => s.status === \"pending\");\n\n if (hasLayout && (hasAnyCached || !hasPending)) {\n this._hasLoadedThumbnails = true;\n this.dispatchEvent(new CustomEvent(\"thumbnails-ready\", { bubbles: true }));\n }\n }\n\n /**\n * Calculate thumbnail layout based on current dimensions and time range.\n * Only recreates slots if layout parameters have actually changed.\n */\n private _calculateLayout(): void {\n if (this._width <= 0 || this._height <= 0 || !this._targetElement) {\n this._thumbnailSlots = [];\n this._lastLayoutParams = null;\n return;\n }\n\n const timeRange = this._getTimeRange();\n if (timeRange.endMs <= timeRange.startMs) {\n this._thumbnailSlots = [];\n this._lastLayoutParams = null;\n return;\n }\n\n // Calculate thumbnail dimensions\n const thumbWidth = this._getEffectiveThumbnailWidth();\n const gap = this.gap;\n\n // Check if layout parameters have changed\n const currentParams = {\n width: this._width,\n height: this._height,\n startTimeMs: timeRange.startMs,\n endTimeMs: timeRange.endMs,\n thumbWidth,\n gap,\n };\n\n // If layout parameters haven't changed, preserve existing slots\n if (this._lastLayoutParams &&\n this._lastLayoutParams.width === currentParams.width &&\n this._lastLayoutParams.height === currentParams.height &&\n this._lastLayoutParams.startTimeMs === currentParams.startTimeMs &&\n this._lastLayoutParams.endTimeMs === currentParams.endTimeMs &&\n this._lastLayoutParams.thumbWidth === currentParams.thumbWidth &&\n this._lastLayoutParams.gap === currentParams.gap) {\n // Layout hasn't changed, keep existing slots\n return;\n }\n\n // Layout changed - recreate slots\n this._lastLayoutParams = currentParams;\n\n // Calculate how many thumbnails fit\n const count = Math.max(1, Math.floor((this._width + gap) / (thumbWidth + gap)));\n\n // Calculate pitch (spacing) for edge-to-edge fill\n const pitch = count > 1 ? (this._width - thumbWidth) / (count - 1) : 0;\n\n // Generate slots with timestamps\n const slots: ThumbnailSlot[] = [];\n const duration = timeRange.endMs - timeRange.startMs;\n\n for (let i = 0; i < count; i++) {\n const timeMs = count === 1\n ? (timeRange.startMs + timeRange.endMs) / 2\n : timeRange.startMs + (i * duration) / (count - 1);\n\n slots.push({\n timeMs,\n x: Math.round(i * pitch),\n width: thumbWidth,\n status: \"pending\",\n });\n }\n\n this._thumbnailSlots = slots;\n }\n\n /**\n * Get effective time range for thumbnails.\n */\n private _getTimeRange(): { startMs: number; endMs: number } {\n const target = this._targetElement;\n if (!target) return { startMs: 0, endMs: 0 };\n\n if (isEFVideo(target)) {\n if (this.useIntrinsicDuration) {\n // Intrinsic mode: 0 to full source duration\n return {\n startMs: this.startTimeMs ?? 0,\n endMs: this.endTimeMs ?? target.intrinsicDurationMs ?? 0,\n };\n }\n // Trimmed mode: source coordinates\n const sourceStart = target.sourceStartMs ?? 0;\n const trimmedDuration = target.durationMs ?? 0;\n return {\n startMs: this.startTimeMs !== undefined ? sourceStart + this.startTimeMs : sourceStart,\n endMs: this.endTimeMs !== undefined ? sourceStart + this.endTimeMs : sourceStart + trimmedDuration,\n };\n }\n\n // Timegroup\n return {\n startMs: this.startTimeMs ?? 0,\n endMs: (this.endTimeMs && this.endTimeMs > 0) ? this.endTimeMs : target.durationMs ?? 0,\n };\n }\n\n /**\n * Calculate effective thumbnail width (auto or specified).\n */\n private _getEffectiveThumbnailWidth(): number {\n if (this.thumbnailWidth > 0) return this.thumbnailWidth;\n\n const target = this._targetElement;\n let aspectRatio = DEFAULT_ASPECT_RATIO;\n\n if (isEFVideo(target)) {\n const w = (target as any).videoWidth || 1920;\n const h = (target as any).videoHeight || 1080;\n aspectRatio = w / h;\n } else if (isEFTimegroup(target)) {\n const w = target.offsetWidth || 1920;\n const h = target.offsetHeight || 1080;\n aspectRatio = w / h;\n }\n\n return Math.round(this._height * aspectRatio);\n }\n\n /**\n * Check cache for existing thumbnails.\n */\n private _checkCache(): void {\n if (!this._targetElement) return;\n\n const { rootId, elementId, epoch } = getCacheIdentifiers(this._targetElement);\n\n for (const slot of this._thumbnailSlots) {\n const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);\n if (sessionThumbnailCache.has(key)) {\n slot.imageData = sessionThumbnailCache.get(key);\n slot.status = \"cached\";\n } else {\n // Mark as pending if not in cache\n slot.status = \"pending\";\n }\n }\n }\n\n /**\n * Draw the canvas with current thumbnail state.\n * Canvas is absolutely positioned at the visible portion of the strip.\n * Uses virtual rendering - only draws thumbnails in the visible region.\n */\n private _drawCanvas(): void {\n const canvas = this.canvasRef.value;\n if (!canvas) return;\n\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return;\n\n const stripWidth = this._width;\n const height = this._height;\n\n if (stripWidth <= 0 || height <= 0) return;\n\n const dpr = window.devicePixelRatio || 1;\n \n // Get scroll and viewport info\n const scrollLeft = this._currentScrollLeft;\n const viewportWidth = this._viewportWidth;\n \n // Get this strip's absolute position in the timeline\n const stripStartPx = this._getStripTimelinePosition();\n const stripEndPx = stripStartPx + stripWidth;\n \n // Calculate visible region in timeline coordinates (with padding)\n const visibleLeftPx = scrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx = scrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n \n // Check if strip is visible at all\n if (stripEndPx < visibleLeftPx || stripStartPx > visibleRightPx) {\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n \n // Calculate the intersection: what part of the strip is visible\n // Coordinates relative to strip's left edge (0 = strip start)\n const visibleStartInStrip = Math.max(0, visibleLeftPx - stripStartPx);\n const visibleEndInStrip = Math.min(stripWidth, visibleRightPx - stripStartPx);\n const visibleWidthPx = visibleEndInStrip - visibleStartInStrip;\n \n if (visibleWidthPx <= 0) {\n canvas.style.display = \"none\";\n return;\n }\n\n // Set canvas size with DPR\n const targetWidth = Math.ceil(visibleWidthPx * dpr);\n const targetHeight = Math.ceil(height * dpr);\n\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n }\n \n // Position canvas at the visible portion within the strip\n canvas.style.left = `${visibleStartInStrip}px`;\n canvas.style.width = `${visibleWidthPx}px`;\n canvas.style.height = `${height}px`;\n\n // Reset transform\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n // Clear with background\n ctx.fillStyle = \"#1a1a2e\";\n ctx.fillRect(0, 0, visibleWidthPx, height);\n\n // Draw each visible thumbnail\n for (const slot of this._thumbnailSlots) {\n const slotRight = slot.x + slot.width;\n\n // Skip if slot is outside visible strip region\n if (slotRight < visibleStartInStrip || slot.x > visibleEndInStrip) continue;\n\n // Draw position relative to canvas (canvas starts at visibleStartInStrip)\n const drawX = slot.x - visibleStartInStrip;\n\n // Skip if outside canvas bounds\n if (drawX + slot.width < 0 || drawX > visibleWidthPx) continue;\n\n if (slot.imageData) {\n this._drawThumbnailImage(ctx, slot.imageData, drawX, slot.width, height);\n } else {\n // Placeholder\n ctx.fillStyle = slot.status === \"loading\" ? \"#2d2d50\" : \"#2d2d44\";\n ctx.fillRect(drawX, 0, slot.width, height);\n\n // Loading indicator\n if (slot.status === \"loading\") {\n ctx.fillStyle = \"rgba(59, 130, 246, 0.3)\";\n ctx.fillRect(drawX, 0, slot.width, 2);\n }\n }\n }\n }\n\n /**\n * Draw a thumbnail image with cover mode scaling.\n */\n private _drawThumbnailImage(\n ctx: CanvasRenderingContext2D,\n imageData: ImageData,\n x: number,\n width: number,\n height: number,\n ): void {\n // Create temp canvas for ImageData\n const tempCanvas = document.createElement(\"canvas\");\n tempCanvas.width = imageData.width;\n tempCanvas.height = imageData.height;\n const tempCtx = tempCanvas.getContext(\"2d\");\n if (!tempCtx) return;\n tempCtx.putImageData(imageData, 0, 0);\n\n // Cover mode: crop to fill destination\n const srcAspect = imageData.width / imageData.height;\n const dstAspect = width / height;\n\n let srcX = 0, srcY = 0, srcW = imageData.width, srcH = imageData.height;\n\n if (srcAspect > dstAspect) {\n srcW = imageData.height * dstAspect;\n srcX = (imageData.width - srcW) / 2;\n } else {\n srcH = imageData.width / dstAspect;\n srcY = (imageData.height - srcH) / 2;\n }\n\n ctx.drawImage(tempCanvas, srcX, srcY, srcW, srcH, x, 0, width, height);\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Thumbnail Loading\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Load thumbnails that are visible in the current viewport.\n * Skips loading if the epoch hasn't changed since last load.\n */\n private async _loadVisibleThumbnails(): Promise<void> {\n if (this._captureInProgress || !this._targetElement) return;\n\n // For timegroups, check if epoch has changed since last load\n // If not, don't reload - the cache should already have the thumbnails\n if (isEFTimegroup(this._targetElement)) {\n const currentEpoch = this._targetElement.contentEpoch;\n if (this._lastLoadedEpoch !== null && this._lastLoadedEpoch === currentEpoch) {\n // Epoch hasn't changed, check if all visible slots are already cached\n // Only proceed if there are actually pending slots that need loading\n const hasPendingSlots = this._thumbnailSlots.some(s => s.status === \"pending\");\n if (!hasPendingSlots) {\n return;\n }\n }\n }\n\n const viewportWidth = this._viewportWidth;\n const scrollOffset = this._currentScrollLeft;\n const stripWidth = this._width;\n \n // Get strip's timeline position\n const stripStartPx = this._getStripTimelinePosition();\n \n // Calculate visible region in timeline coordinates\n const visibleLeftPx = scrollOffset - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx = scrollOffset + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n \n // Convert to strip-local coordinates\n const visibleStartInStrip = Math.max(0, visibleLeftPx - stripStartPx);\n const visibleEndInStrip = Math.min(stripWidth, visibleRightPx - stripStartPx);\n\n // Find pending slots in visible range (using strip-local coordinates)\n const pending = this._thumbnailSlots.filter((slot) => {\n if (slot.status !== \"pending\") return false;\n const slotRight = slot.x + slot.width;\n return slotRight >= visibleStartInStrip && slot.x <= visibleEndInStrip;\n });\n\n if (pending.length === 0) return;\n\n this._captureInProgress = true;\n\n // Mark as loading\n for (const slot of pending) {\n slot.status = \"loading\";\n }\n this._drawCanvas();\n\n try {\n if (isEFTimegroup(this._targetElement)) {\n await this._captureTimegroupThumbnails(pending);\n } else if (isEFVideo(this._targetElement)) {\n await this._captureVideoThumbnails(pending);\n }\n } catch (error) {\n console.warn(\"Failed to capture thumbnails:\", error);\n // Reset failed slots\n for (const slot of pending) {\n if (slot.status === \"loading\") {\n slot.status = \"pending\";\n }\n }\n } finally {\n this._captureInProgress = false;\n this._drawCanvas();\n\n // Update last loaded epoch for timegroups\n if (isEFTimegroup(this._targetElement)) {\n this._lastLoadedEpoch = this._targetElement.contentEpoch;\n }\n\n // Dispatch ready event when thumbnails are first loaded\n const hasAnyLoaded = this._thumbnailSlots.some((s) => s.status === \"cached\");\n if (hasAnyLoaded && !this._hasLoadedThumbnails) {\n this._hasLoadedThumbnails = true;\n this.dispatchEvent(new CustomEvent(\"thumbnails-ready\", { bubbles: true }));\n }\n }\n }\n\n /**\n * Capture thumbnails from a timegroup target.\n */\n private async _captureTimegroupThumbnails(slots: ThumbnailSlot[]): Promise<void> {\n const target = this._targetElement as EFTimegroup;\n const { rootId, elementId, epoch } = getCacheIdentifiers(target);\n\n // Calculate capture scale\n const timegroupWidth = target.offsetWidth || 1920;\n const timegroupHeight = target.offsetHeight || 1080;\n const scale = Math.min(1, this._height / timegroupHeight, MAX_CAPTURE_WIDTH / timegroupWidth);\n\n // Process in batches\n for (let i = 0; i < slots.length; i += BATCH_SIZE) {\n const batch = slots.slice(i, i + BATCH_SIZE);\n const timestamps = batch.map((s) => s.timeMs);\n\n try {\n const canvases = await target.captureBatch(timestamps, {\n scale,\n contentReadyMode: \"immediate\",\n });\n\n for (let j = 0; j < batch.length; j++) {\n const slot = batch[j]!;\n const canvas = canvases[j];\n\n if (canvas) {\n const imageData = this._canvasToImageData(canvas);\n if (imageData) {\n const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);\n sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);\n slot.imageData = imageData;\n slot.status = \"cached\";\n }\n }\n }\n\n // Redraw after each batch for progressive feedback\n this._drawCanvas();\n\n // Yield to main thread between batches\n if (i + BATCH_SIZE < slots.length) {\n await new Promise((r) => requestAnimationFrame(r));\n }\n } catch (error) {\n console.warn(\"Batch capture failed:\", error);\n }\n }\n }\n\n /**\n * Capture thumbnails from a video target using MediaEngine.\n */\n private async _captureVideoThumbnails(slots: ThumbnailSlot[]): Promise<void> {\n const target = this._targetElement as EFVideo;\n const { rootId, elementId, epoch } = getCacheIdentifiers(target);\n\n // Wait for media engine\n if (target.mediaEngineTask) {\n await target.mediaEngineTask.taskComplete;\n }\n\n const mediaEngine = target.mediaEngineTask?.value;\n if (!mediaEngine) return;\n\n // Check for video rendition\n const videoRendition = mediaEngine.getVideoRendition();\n const scrubRendition = mediaEngine.getScrubVideoRendition();\n if (!videoRendition && !scrubRendition) return;\n\n const timestamps = slots.map((s) => s.timeMs);\n\n // Create an abort controller for this thumbnail extraction\n // ThumbnailExtractor requires a signal to properly handle cleanup\n const abortController = new AbortController();\n\n try {\n const results = await mediaEngine.extractThumbnails(timestamps, abortController.signal);\n\n for (let i = 0; i < slots.length; i++) {\n const slot = slots[i]!;\n const result = results[i];\n\n if (result?.thumbnail) {\n const imageData = this._canvasToImageData(result.thumbnail);\n if (imageData) {\n const key = getCacheKey(rootId, elementId, slot.timeMs, epoch);\n sessionThumbnailCache.set(key, imageData, slot.timeMs, elementId);\n slot.imageData = imageData;\n slot.status = \"cached\";\n }\n }\n }\n } catch (error) {\n // Abort on error to clean up any in-flight requests\n abortController.abort();\n console.warn(\"Video thumbnail extraction failed:\", error);\n }\n }\n\n /**\n * Convert canvas to ImageData.\n */\n private _canvasToImageData(canvas: HTMLCanvasElement | OffscreenCanvas): ImageData | null {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true }) as\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | null;\n if (!ctx) return null;\n return ctx.getImageData(0, 0, canvas.width, canvas.height);\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public API\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Returns a promise that resolves when thumbnails are ready.\n * Resolves immediately if thumbnails are already loaded.\n */\n whenReady(): Promise<void> {\n if (this._hasLoadedThumbnails) {\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n this.addEventListener(\"thumbnails-ready\", () => resolve(), { once: true });\n });\n }\n\n /**\n * Check if thumbnails have been loaded.\n */\n get isReady(): boolean {\n return this._hasLoadedThumbnails;\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Cache Invalidation\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Invalidate cached thumbnails for this element within a time range.\n * Call this when content changes at specific times.\n */\n invalidateTimeRange(startTimeMs: number, endTimeMs: number): void {\n if (!this._targetElement) return;\n\n const { rootId, elementId } = getCacheIdentifiers(this._targetElement);\n sessionThumbnailCache.invalidateTimeRange(rootId, elementId, startTimeMs, endTimeMs);\n\n // Reset affected slots\n for (const slot of this._thumbnailSlots) {\n if (slot.timeMs >= startTimeMs && slot.timeMs <= endTimeMs) {\n slot.imageData = undefined;\n slot.status = \"pending\";\n }\n }\n\n this._scheduleRender();\n }\n\n /**\n * Invalidate all cached thumbnails for this element.\n */\n invalidateAll(): void {\n if (!this._targetElement) return;\n\n const { rootId, elementId } = getCacheIdentifiers(this._targetElement);\n sessionThumbnailCache.invalidateElement(rootId, elementId);\n\n // Reset all slots\n for (const slot of this._thumbnailSlots) {\n slot.imageData = undefined;\n slot.status = \"pending\";\n }\n\n this._scheduleRender();\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Render\n // ─────────────────────────────────────────────────────────────────────────\n\n render() {\n return html`<canvas ${ref(this.canvasRef)}></canvas>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-thumbnail-strip\": EFThumbnailStrip;\n }\n}\n\n// Re-export cache for backwards compatibility and debugging\nexport { sessionThumbnailCache as thumbnailImageCache } from \"./SessionThumbnailCache.js\";\n"],"mappings":"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"}
|
|
@@ -112,6 +112,18 @@ declare class EFTimegroup extends EFTimegroup_base {
|
|
|
112
112
|
* @public
|
|
113
113
|
*/
|
|
114
114
|
get effectiveFps(): number;
|
|
115
|
+
/**
|
|
116
|
+
* Get the current content epoch (used by thumbnail cache).
|
|
117
|
+
* The epoch increments whenever visual content changes.
|
|
118
|
+
* @public
|
|
119
|
+
*/
|
|
120
|
+
get contentEpoch(): number;
|
|
121
|
+
/**
|
|
122
|
+
* Increment content epoch (called when visual content changes).
|
|
123
|
+
* This invalidates cached thumbnails by changing their cache keys.
|
|
124
|
+
* @public
|
|
125
|
+
*/
|
|
126
|
+
incrementContentEpoch(): void;
|
|
115
127
|
/** @public */
|
|
116
128
|
set currentTime(time: number);
|
|
117
129
|
/** @public */
|
|
@@ -308,6 +308,8 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
|
|
|
308
308
|
super.attributeChangedCallback(name, old, value);
|
|
309
309
|
}
|
|
310
310
|
#resizeObserver;
|
|
311
|
+
/** Content epoch - increments when visual content changes (used by thumbnail cache) */
|
|
312
|
+
#contentEpoch = 0;
|
|
311
313
|
#currentTime = void 0;
|
|
312
314
|
#userTimeMs = 0;
|
|
313
315
|
#seekInProgress = false;
|
|
@@ -336,6 +338,22 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
|
|
|
336
338
|
if (typeof window !== "undefined" && window.EF_FRAMEGEN?.renderOptions) return window.EF_FRAMEGEN.renderOptions.encoderOptions.video.framerate;
|
|
337
339
|
return this.fps;
|
|
338
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Get the current content epoch (used by thumbnail cache).
|
|
343
|
+
* The epoch increments whenever visual content changes.
|
|
344
|
+
* @public
|
|
345
|
+
*/
|
|
346
|
+
get contentEpoch() {
|
|
347
|
+
return this.#contentEpoch;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Increment content epoch (called when visual content changes).
|
|
351
|
+
* This invalidates cached thumbnails by changing their cache keys.
|
|
352
|
+
* @public
|
|
353
|
+
*/
|
|
354
|
+
incrementContentEpoch() {
|
|
355
|
+
this.#contentEpoch++;
|
|
356
|
+
}
|
|
339
357
|
async #runThrottledFrameTask() {
|
|
340
358
|
if (this.playbackController) return this.playbackController.runThrottledFrameTask();
|
|
341
359
|
await this.frameTask.run();
|
|
@@ -1086,7 +1104,16 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
|
|
|
1086
1104
|
/** @internal */
|
|
1087
1105
|
wrapWithWorkbench() {
|
|
1088
1106
|
const workbench = document.createElement("ef-workbench");
|
|
1089
|
-
this.parentElement
|
|
1107
|
+
const parent = this.parentElement;
|
|
1108
|
+
if (parent === document.body) {
|
|
1109
|
+
workbench.style.position = "fixed";
|
|
1110
|
+
workbench.style.top = "0";
|
|
1111
|
+
workbench.style.left = "0";
|
|
1112
|
+
workbench.style.width = "100vw";
|
|
1113
|
+
workbench.style.height = "100vh";
|
|
1114
|
+
workbench.style.zIndex = "0";
|
|
1115
|
+
}
|
|
1116
|
+
parent?.append(workbench);
|
|
1090
1117
|
if (!this.hasAttribute("id")) this.setAttribute("id", "root-timegroup");
|
|
1091
1118
|
const panZoom = document.createElement("ef-pan-zoom");
|
|
1092
1119
|
panZoom.id = "workbench-panzoom";
|