@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.
@@ -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) and elementId (for element-specific caching).
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: (rootTemporal && isEFTimegroup(rootTemporal) ? rootTemporal : null)?.id || "default",
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) this._hasLoadedThumbnails = false;
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(() => this._scheduleRender());
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":"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"}
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?.append(workbench);
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";