unified-video-framework 1.4.446 → 1.4.448

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/interfaces.d.ts +14 -0
  3. package/packages/core/dist/interfaces.d.ts.map +1 -1
  4. package/packages/core/dist/version.d.ts +1 -1
  5. package/packages/core/dist/version.js +1 -1
  6. package/packages/core/src/interfaces.ts +24 -0
  7. package/packages/core/src/version.ts +1 -1
  8. package/packages/web/dist/WebPlayer.d.ts +364 -0
  9. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  10. package/packages/web/dist/WebPlayer.js +347 -7
  11. package/packages/web/dist/WebPlayer.js.map +1 -1
  12. package/packages/web/dist/chapters/ChapterManager.js +3 -3
  13. package/packages/web/dist/chapters/SkipButtonController.js +1 -1
  14. package/packages/web/dist/chapters/index.js +7 -7
  15. package/packages/web/dist/index.js +4 -4
  16. package/packages/web/dist/paywall/PaywallController.js +1 -1
  17. package/packages/web/dist/react/EPG.js +6 -6
  18. package/packages/web/dist/react/WebPlayerView.d.ts +399 -0
  19. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  20. package/packages/web/dist/react/WebPlayerView.js +19 -6
  21. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  22. package/packages/web/dist/react/WebPlayerViewWithEPG.js +2 -2
  23. package/packages/web/dist/react/components/ChapterProgress.js +1 -1
  24. package/packages/web/dist/react/components/EPGOverlay-improved-positioning.js +5 -5
  25. package/packages/web/dist/react/components/EPGOverlay.js +5 -5
  26. package/packages/web/dist/react/components/EPGProgramDetails.js +1 -1
  27. package/packages/web/dist/react/components/EPGProgramGrid.js +1 -1
  28. package/packages/web/dist/react/components/EPGTimelineHeader.js +1 -1
  29. package/packages/web/dist/react/components/SkipButton.js +1 -1
  30. package/packages/web/dist/react/examples/google-ads-example.js +1 -1
  31. package/packages/web/dist/react/types/ThumbnailPreviewTypes.d.ts +26 -0
  32. package/packages/web/dist/react/types/ThumbnailPreviewTypes.d.ts.map +1 -0
  33. package/packages/web/dist/react/types/ThumbnailPreviewTypes.js +2 -0
  34. package/packages/web/dist/react/types/ThumbnailPreviewTypes.js.map +1 -0
  35. package/packages/web/dist/test/epg-test.js +1 -1
  36. package/packages/web/src/WebPlayer.ts +432 -4
  37. package/packages/web/src/react/WebPlayerView.tsx +21 -2
  38. package/packages/web/src/react/types/ThumbnailPreviewTypes.ts +62 -0
@@ -33,6 +33,11 @@ import type {
33
33
  ItemCyclingConfig,
34
34
  SeparatorType
35
35
  } from './react/types/FlashNewsTickerTypes';
36
+ import type {
37
+ ThumbnailPreviewConfig,
38
+ ThumbnailEntry,
39
+ ThumbnailGenerationImage
40
+ } from './react/types/ThumbnailPreviewTypes';
36
41
  import YouTubeExtractor from './utils/YouTubeExtractor';
37
42
  import { DRMManager, DRMErrorHandler } from './drm';
38
43
  import type { DRMInitResult, DRMError } from './drm';
@@ -204,6 +209,13 @@ export class WebPlayer extends BasePlayer {
204
209
  private lastDuration: number = 0; // Track duration changes to detect live streams
205
210
  private isDetectedAsLive: boolean = false; // Once detected as live, stays live
206
211
 
212
+ // Thumbnail preview
213
+ private thumbnailPreviewConfig: ThumbnailPreviewConfig | null = null;
214
+ private thumbnailEntries: ThumbnailEntry[] = [];
215
+ private preloadedThumbnails: Map<string, HTMLImageElement> = new Map();
216
+ private currentThumbnailUrl: string | null = null;
217
+ private thumbnailPreviewEnabled: boolean = false;
218
+
207
219
  // Debug logging helper
208
220
  private debugLog(message: string, ...args: any[]): void {
209
221
  if (this.config.debug) {
@@ -223,6 +235,242 @@ export class WebPlayer extends BasePlayer {
223
235
  }
224
236
  }
225
237
 
238
+ // ============================================
239
+ // Thumbnail Preview Methods
240
+ // ============================================
241
+
242
+ /**
243
+ * Initialize thumbnail preview with config
244
+ */
245
+ public initializeThumbnailPreview(config: ThumbnailPreviewConfig): void {
246
+ if (!config || !config.generationImage) {
247
+ this.thumbnailPreviewEnabled = false;
248
+ return;
249
+ }
250
+
251
+ this.thumbnailPreviewConfig = config;
252
+ this.thumbnailPreviewEnabled = config.enabled !== false;
253
+
254
+ // Transform generation image data to sorted array for efficient lookup
255
+ this.thumbnailEntries = this.transformThumbnailData(config.generationImage);
256
+
257
+ this.debugLog('Thumbnail preview initialized:', {
258
+ enabled: this.thumbnailPreviewEnabled,
259
+ entries: this.thumbnailEntries.length
260
+ });
261
+
262
+ // Apply custom styles if provided
263
+ if (config.style) {
264
+ this.applyThumbnailStyles(config.style);
265
+ }
266
+
267
+ // Preload images if enabled (default: true)
268
+ if (config.preloadImages !== false && this.thumbnailEntries.length > 0) {
269
+ this.preloadThumbnailImages();
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Transform generation image JSON to sorted array of ThumbnailEntry
275
+ */
276
+ private transformThumbnailData(generationImage: ThumbnailGenerationImage): ThumbnailEntry[] {
277
+ const entries: ThumbnailEntry[] = [];
278
+
279
+ for (const [url, timeRange] of Object.entries(generationImage)) {
280
+ const parts = timeRange.split('-');
281
+ if (parts.length === 2) {
282
+ const startTime = parseFloat(parts[0]);
283
+ const endTime = parseFloat(parts[1]);
284
+ if (!isNaN(startTime) && !isNaN(endTime)) {
285
+ entries.push({ url, startTime, endTime });
286
+ }
287
+ }
288
+ }
289
+
290
+ // Sort by startTime for efficient binary search
291
+ entries.sort((a, b) => a.startTime - b.startTime);
292
+
293
+ return entries;
294
+ }
295
+
296
+ /**
297
+ * Find thumbnail for a given time using binary search (O(log n))
298
+ */
299
+ private findThumbnailForTime(time: number): ThumbnailEntry | null {
300
+ if (this.thumbnailEntries.length === 0) return null;
301
+
302
+ let left = 0;
303
+ let right = this.thumbnailEntries.length - 1;
304
+ let result: ThumbnailEntry | null = null;
305
+
306
+ while (left <= right) {
307
+ const mid = Math.floor((left + right) / 2);
308
+ const entry = this.thumbnailEntries[mid];
309
+
310
+ if (time >= entry.startTime && time < entry.endTime) {
311
+ return entry;
312
+ } else if (time < entry.startTime) {
313
+ right = mid - 1;
314
+ } else {
315
+ left = mid + 1;
316
+ }
317
+ }
318
+
319
+ // If no exact match, find the closest entry
320
+ if (left > 0 && left <= this.thumbnailEntries.length) {
321
+ const prevEntry = this.thumbnailEntries[left - 1];
322
+ if (time >= prevEntry.startTime && time < prevEntry.endTime) {
323
+ return prevEntry;
324
+ }
325
+ }
326
+
327
+ return result;
328
+ }
329
+
330
+ /**
331
+ * Preload all thumbnail images for instant switching
332
+ */
333
+ private preloadThumbnailImages(): void {
334
+ this.debugLog('Preloading', this.thumbnailEntries.length, 'thumbnail images');
335
+
336
+ for (const entry of this.thumbnailEntries) {
337
+ if (this.preloadedThumbnails.has(entry.url)) continue;
338
+
339
+ const img = new Image();
340
+ img.onload = () => {
341
+ this.preloadedThumbnails.set(entry.url, img);
342
+ this.debugLog('Preloaded thumbnail:', entry.url);
343
+ };
344
+ img.onerror = () => {
345
+ this.debugWarn('Failed to preload thumbnail:', entry.url);
346
+ };
347
+ img.src = entry.url;
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Apply custom thumbnail styles
353
+ */
354
+ private applyThumbnailStyles(style: ThumbnailPreviewConfig['style']): void {
355
+ if (!style) return;
356
+
357
+ const wrapper = document.getElementById('uvf-thumbnail-preview');
358
+ const imageWrapper = wrapper?.querySelector('.uvf-thumbnail-preview-image-wrapper') as HTMLElement;
359
+
360
+ if (imageWrapper) {
361
+ if (style.width) {
362
+ imageWrapper.style.width = `${style.width}px`;
363
+ }
364
+ if (style.height) {
365
+ imageWrapper.style.height = `${style.height}px`;
366
+ }
367
+ if (style.borderRadius !== undefined) {
368
+ imageWrapper.style.borderRadius = `${style.borderRadius}px`;
369
+ }
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Update thumbnail preview based on mouse position
375
+ */
376
+ private updateThumbnailPreview(e: MouseEvent): void {
377
+ if (!this.thumbnailPreviewEnabled || this.thumbnailEntries.length === 0) {
378
+ return;
379
+ }
380
+
381
+ const progressBar = document.getElementById('uvf-progress-bar');
382
+ const thumbnailPreview = document.getElementById('uvf-thumbnail-preview');
383
+ const thumbnailImage = document.getElementById('uvf-thumbnail-image') as HTMLImageElement;
384
+ const thumbnailTime = document.getElementById('uvf-thumbnail-time');
385
+
386
+ if (!progressBar || !thumbnailPreview || !thumbnailImage || !this.video) {
387
+ return;
388
+ }
389
+
390
+ const rect = progressBar.getBoundingClientRect();
391
+ const x = e.clientX - rect.left;
392
+ const percent = Math.max(0, Math.min(1, x / rect.width));
393
+ const time = percent * this.video.duration;
394
+
395
+ // Find thumbnail for this time
396
+ const entry = this.findThumbnailForTime(time);
397
+
398
+ if (entry) {
399
+ // Show thumbnail preview
400
+ thumbnailPreview.classList.add('visible');
401
+
402
+ // Calculate position (clamp to prevent overflow)
403
+ const thumbnailWidth = 160; // Default width
404
+ const halfWidth = thumbnailWidth / 2;
405
+ const minX = halfWidth;
406
+ const maxX = rect.width - halfWidth;
407
+ const clampedX = Math.max(minX, Math.min(maxX, x));
408
+
409
+ thumbnailPreview.style.left = `${clampedX}px`;
410
+
411
+ // Update image only if URL changed
412
+ if (this.currentThumbnailUrl !== entry.url) {
413
+ this.currentThumbnailUrl = entry.url;
414
+ thumbnailImage.classList.remove('loaded');
415
+
416
+ // Check if image is preloaded
417
+ const preloaded = this.preloadedThumbnails.get(entry.url);
418
+ if (preloaded) {
419
+ thumbnailImage.src = preloaded.src;
420
+ thumbnailImage.classList.add('loaded');
421
+ } else {
422
+ thumbnailImage.onload = () => {
423
+ thumbnailImage.classList.add('loaded');
424
+ };
425
+ thumbnailImage.src = entry.url;
426
+ }
427
+ }
428
+
429
+ // Update time display
430
+ if (thumbnailTime && this.thumbnailPreviewConfig?.showTimeInThumbnail !== false) {
431
+ thumbnailTime.textContent = this.formatTime(time);
432
+ thumbnailTime.style.display = 'block';
433
+ } else if (thumbnailTime) {
434
+ thumbnailTime.style.display = 'none';
435
+ }
436
+ } else {
437
+ // No thumbnail for this time - hide preview and let regular tooltip show
438
+ this.hideThumbnailPreview();
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Hide thumbnail preview
444
+ */
445
+ private hideThumbnailPreview(): void {
446
+ const thumbnailPreview = document.getElementById('uvf-thumbnail-preview');
447
+ if (thumbnailPreview) {
448
+ thumbnailPreview.classList.remove('visible');
449
+ }
450
+ this.currentThumbnailUrl = null;
451
+ }
452
+
453
+ /**
454
+ * Set thumbnail preview config at runtime
455
+ */
456
+ public setThumbnailPreview(config: ThumbnailPreviewConfig | null): void {
457
+ if (!config) {
458
+ this.thumbnailPreviewEnabled = false;
459
+ this.thumbnailPreviewConfig = null;
460
+ this.thumbnailEntries = [];
461
+ this.preloadedThumbnails.clear();
462
+ this.hideThumbnailPreview();
463
+ return;
464
+ }
465
+
466
+ this.initializeThumbnailPreview(config);
467
+ }
468
+
469
+ // Note: Uses existing formatTime method defined elsewhere in this class
470
+
471
+ // ============================================
472
+ // End Thumbnail Preview Methods
473
+ // ============================================
226
474
 
227
475
  async initialize(container: HTMLElement | string, config?: any): Promise<void> {
228
476
  // Debug log the config being passed
@@ -300,6 +548,12 @@ export class WebPlayer extends BasePlayer {
300
548
  this.youtubeNativeControls = config.youtubeNativeControls;
301
549
  }
302
550
 
551
+ // Configure thumbnail preview if provided
552
+ if (config && config.thumbnailPreview) {
553
+ console.log('Thumbnail preview config found:', config.thumbnailPreview);
554
+ this.initializeThumbnailPreview(config.thumbnailPreview);
555
+ }
556
+
303
557
  // Call parent initialize
304
558
  await super.initialize(container, config);
305
559
  }
@@ -5451,7 +5705,37 @@ export class WebPlayer extends BasePlayer {
5451
5705
  top: 2px; /* Center on the 4px hover progress bar (2px from top) */
5452
5706
  transform: translate(-50%, -50%) scale(1);
5453
5707
  }
5454
-
5708
+
5709
+ /* Prevent text selection during seekbar drag */
5710
+ .uvf-player-wrapper.seeking {
5711
+ user-select: none;
5712
+ -webkit-user-select: none;
5713
+ -moz-user-select: none;
5714
+ -ms-user-select: none;
5715
+ }
5716
+
5717
+ /* Maintain expanded seekbar state during drag (same as hover) */
5718
+ .uvf-progress-bar-wrapper.dragging .uvf-progress-bar {
5719
+ height: 4px;
5720
+ background: rgba(255, 255, 255, 0.2);
5721
+ border-radius: 6px;
5722
+ transform: scaleY(1.1);
5723
+ }
5724
+
5725
+ .uvf-progress-bar-wrapper.dragging .uvf-progress-handle {
5726
+ opacity: 1;
5727
+ top: 2px;
5728
+ transform: translate(-50%, -50%) scale(1);
5729
+ }
5730
+
5731
+ .uvf-progress-bar-wrapper.dragging .uvf-progress-buffered {
5732
+ border-radius: 6px;
5733
+ }
5734
+
5735
+ .uvf-progress-bar-wrapper.dragging .uvf-progress-filled {
5736
+ border-radius: 6px;
5737
+ }
5738
+
5455
5739
  .uvf-progress-handle:hover {
5456
5740
  transform: translate(-50%, -50%) scale(1.2);
5457
5741
  box-shadow: 0 3px 12px rgba(0, 0, 0, 0.4);
@@ -5506,7 +5790,122 @@ export class WebPlayer extends BasePlayer {
5506
5790
  opacity: 1;
5507
5791
  transform: translateX(-50%) translateY(0);
5508
5792
  }
5509
-
5793
+
5794
+ /* Thumbnail Preview */
5795
+ .uvf-thumbnail-preview {
5796
+ position: absolute;
5797
+ bottom: 100%;
5798
+ left: 0;
5799
+ margin-bottom: 12px;
5800
+ display: flex;
5801
+ flex-direction: column;
5802
+ align-items: center;
5803
+ pointer-events: none;
5804
+ z-index: 25;
5805
+ opacity: 0;
5806
+ transform: translateX(-50%) translateY(8px);
5807
+ transition: opacity 0.15s ease, transform 0.15s ease;
5808
+ }
5809
+
5810
+ .uvf-thumbnail-preview.visible {
5811
+ opacity: 1;
5812
+ transform: translateX(-50%) translateY(0);
5813
+ }
5814
+
5815
+ .uvf-thumbnail-preview-container {
5816
+ position: relative;
5817
+ background: rgba(0, 0, 0, 0.9);
5818
+ border-radius: 8px;
5819
+ padding: 4px;
5820
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.1);
5821
+ backdrop-filter: blur(12px);
5822
+ -webkit-backdrop-filter: blur(12px);
5823
+ }
5824
+
5825
+ .uvf-thumbnail-preview-image-wrapper {
5826
+ position: relative;
5827
+ width: 160px;
5828
+ height: 90px;
5829
+ border-radius: 6px;
5830
+ overflow: hidden;
5831
+ background: rgba(30, 30, 30, 0.95);
5832
+ }
5833
+
5834
+ .uvf-thumbnail-preview-image {
5835
+ width: 100%;
5836
+ height: 100%;
5837
+ object-fit: cover;
5838
+ display: block;
5839
+ opacity: 0;
5840
+ transition: opacity 0.2s ease;
5841
+ }
5842
+
5843
+ .uvf-thumbnail-preview-image.loaded {
5844
+ opacity: 1;
5845
+ }
5846
+
5847
+ .uvf-thumbnail-preview-loading {
5848
+ position: absolute;
5849
+ top: 50%;
5850
+ left: 50%;
5851
+ transform: translate(-50%, -50%);
5852
+ width: 24px;
5853
+ height: 24px;
5854
+ border: 2px solid rgba(255, 255, 255, 0.2);
5855
+ border-top-color: var(--uvf-accent-1, #ff5722);
5856
+ border-radius: 50%;
5857
+ animation: uvf-spin 0.8s linear infinite;
5858
+ }
5859
+
5860
+ .uvf-thumbnail-preview-image.loaded + .uvf-thumbnail-preview-loading {
5861
+ display: none;
5862
+ }
5863
+
5864
+ .uvf-thumbnail-preview-time {
5865
+ position: absolute;
5866
+ bottom: 6px;
5867
+ left: 50%;
5868
+ transform: translateX(-50%);
5869
+ background: rgba(0, 0, 0, 0.85);
5870
+ color: #fff;
5871
+ font-size: 12px;
5872
+ font-weight: 600;
5873
+ padding: 3px 8px;
5874
+ border-radius: 4px;
5875
+ white-space: nowrap;
5876
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
5877
+ backdrop-filter: blur(4px);
5878
+ -webkit-backdrop-filter: blur(4px);
5879
+ }
5880
+
5881
+ .uvf-thumbnail-preview-arrow {
5882
+ width: 0;
5883
+ height: 0;
5884
+ border-left: 8px solid transparent;
5885
+ border-right: 8px solid transparent;
5886
+ border-top: 8px solid rgba(0, 0, 0, 0.9);
5887
+ margin-top: -1px;
5888
+ }
5889
+
5890
+ /* Hide regular time tooltip when thumbnail preview is visible */
5891
+ .uvf-thumbnail-preview.visible ~ .uvf-time-tooltip {
5892
+ opacity: 0 !important;
5893
+ pointer-events: none;
5894
+ }
5895
+
5896
+ /* Responsive thumbnail sizes */
5897
+ @media (max-width: 768px) {
5898
+ .uvf-thumbnail-preview-image-wrapper {
5899
+ width: 120px;
5900
+ height: 68px;
5901
+ }
5902
+
5903
+ .uvf-thumbnail-preview-time {
5904
+ font-size: 11px;
5905
+ padding: 2px 6px;
5906
+ }
5907
+ }
5908
+
5510
5909
  /* Chapter Markers */
5511
5910
  .uvf-chapter-marker {
5512
5911
  position: absolute;
@@ -8602,6 +9001,16 @@ export class WebPlayer extends BasePlayer {
8602
9001
  <div class="uvf-progress-filled" id="uvf-progress-filled"></div>
8603
9002
  <div class="uvf-progress-handle" id="uvf-progress-handle"></div>
8604
9003
  </div>
9004
+ <div class="uvf-thumbnail-preview" id="uvf-thumbnail-preview">
9005
+ <div class="uvf-thumbnail-preview-container">
9006
+ <div class="uvf-thumbnail-preview-image-wrapper">
9007
+ <img class="uvf-thumbnail-preview-image" id="uvf-thumbnail-image" alt="Preview" />
9008
+ <div class="uvf-thumbnail-preview-loading"></div>
9009
+ </div>
9010
+ <div class="uvf-thumbnail-preview-time" id="uvf-thumbnail-time">00:00</div>
9011
+ </div>
9012
+ <div class="uvf-thumbnail-preview-arrow"></div>
9013
+ </div>
8605
9014
  <div class="uvf-time-tooltip" id="uvf-time-tooltip">00:00</div>
8606
9015
  `;
8607
9016
  progressSection.appendChild(progressBar);
@@ -8997,6 +9406,9 @@ export class WebPlayer extends BasePlayer {
8997
9406
  progressBar?.addEventListener('mousedown', (e) => {
8998
9407
  this.isDragging = true;
8999
9408
  this.showTimeTooltip = true;
9409
+ // Maintain seekbar expanded state and prevent text selection during drag
9410
+ progressBar.classList.add('dragging');
9411
+ this.playerWrapper?.classList.add('seeking');
9000
9412
  this.seekToPosition(e as MouseEvent);
9001
9413
  this.updateTimeTooltip(e as MouseEvent);
9002
9414
  });
@@ -9010,12 +9422,15 @@ export class WebPlayer extends BasePlayer {
9010
9422
  if (!this.isDragging) {
9011
9423
  this.showTimeTooltip = false;
9012
9424
  this.hideTimeTooltip();
9425
+ this.hideThumbnailPreview();
9013
9426
  }
9014
9427
  });
9015
9428
 
9016
9429
  progressBar?.addEventListener('mousemove', (e) => {
9017
9430
  if (this.showTimeTooltip) {
9018
9431
  this.updateTimeTooltip(e as MouseEvent);
9432
+ // Update thumbnail preview on hover
9433
+ this.updateThumbnailPreview(e as MouseEvent);
9019
9434
  }
9020
9435
  });
9021
9436
 
@@ -9023,6 +9438,9 @@ export class WebPlayer extends BasePlayer {
9023
9438
  progressBar?.addEventListener('touchstart', (e) => {
9024
9439
  e.preventDefault(); // Prevent scrolling
9025
9440
  this.isDragging = true;
9441
+ // Maintain seekbar expanded state and prevent text selection during drag
9442
+ progressBar.classList.add('dragging');
9443
+ this.playerWrapper?.classList.add('seeking');
9026
9444
  const touch = e.touches[0];
9027
9445
  const mouseEvent = new MouseEvent('mousedown', {
9028
9446
  clientX: touch.clientX,
@@ -9040,6 +9458,8 @@ export class WebPlayer extends BasePlayer {
9040
9458
  this.seekToPosition(e);
9041
9459
  // Update tooltip position during dragging
9042
9460
  this.updateTimeTooltip(e);
9461
+ // Update thumbnail preview during dragging
9462
+ this.updateThumbnailPreview(e);
9043
9463
  }
9044
9464
  });
9045
9465
 
@@ -9067,13 +9487,17 @@ export class WebPlayer extends BasePlayer {
9067
9487
 
9068
9488
  if (this.isDragging) {
9069
9489
  this.isDragging = false;
9490
+ // Remove dragging classes
9491
+ progressBar?.classList.remove('dragging');
9492
+ this.playerWrapper?.classList.remove('seeking');
9070
9493
  // Remove dragging class from handle
9071
9494
  const handle = document.getElementById('uvf-progress-handle');
9072
9495
  handle?.classList.remove('dragging');
9073
- // Hide tooltip if mouse is not over progress bar
9496
+ // Hide tooltip and thumbnail preview if mouse is not over progress bar
9074
9497
  if (progressBar && !progressBar.matches(':hover')) {
9075
9498
  this.showTimeTooltip = false;
9076
9499
  this.hideTimeTooltip();
9500
+ this.hideThumbnailPreview();
9077
9501
  }
9078
9502
  }
9079
9503
  });
@@ -9081,12 +9505,16 @@ export class WebPlayer extends BasePlayer {
9081
9505
  document.addEventListener('touchend', () => {
9082
9506
  if (this.isDragging) {
9083
9507
  this.isDragging = false;
9508
+ // Remove dragging classes
9509
+ progressBar?.classList.remove('dragging');
9510
+ this.playerWrapper?.classList.remove('seeking');
9084
9511
  // Remove dragging class from handle
9085
9512
  const handle = document.getElementById('uvf-progress-handle');
9086
9513
  handle?.classList.remove('dragging');
9087
- // Hide tooltip on touch end
9514
+ // Hide tooltip and thumbnail preview on touch end
9088
9515
  this.showTimeTooltip = false;
9089
9516
  this.hideTimeTooltip();
9517
+ this.hideThumbnailPreview();
9090
9518
  }
9091
9519
  });
9092
9520
 
@@ -8,6 +8,7 @@ import { GoogleAdsManager } from '../ads/GoogleAdsManager';
8
8
  import type { EPGData, EPGConfig, EPGProgram, EPGProgramRow } from './types/EPGTypes';
9
9
  import type { VCManifest, VCProduct, VCEvent } from './types/VideoCommerceTypes';
10
10
  import type { FlashNewsTickerConfig } from './types/FlashNewsTickerTypes';
11
+ import type { ThumbnailPreviewConfig } from './types/ThumbnailPreviewTypes';
11
12
  import { useCommerceSync } from './hooks/useCommerceSync';
12
13
  import ProductBadge from './components/commerce/ProductBadge';
13
14
  import ProductPanel from './components/commerce/ProductPanel';
@@ -441,6 +442,9 @@ export type WebPlayerViewProps = {
441
442
 
442
443
  // Flash News Ticker
443
444
  flashNewsTicker?: FlashNewsTickerConfig; // Flash news ticker configuration
445
+
446
+ // Thumbnail Preview on Seekbar Hover
447
+ thumbnailPreview?: ThumbnailPreviewConfig; // Thumbnail preview configuration
444
448
  };
445
449
 
446
450
  export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
@@ -996,8 +1000,10 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
996
1000
  rememberChoices: chaptersConfig.userPreferences?.rememberChoices ?? true,
997
1001
  resumePlaybackAfterSkip: chaptersConfig.userPreferences?.resumePlaybackAfterSkip ?? true,
998
1002
  }
999
- } : { enabled: false }
1000
- };
1003
+ } : { enabled: false },
1004
+ // Thumbnail preview configuration
1005
+ thumbnailPreview: props.thumbnailPreview
1006
+ } as any;
1001
1007
 
1002
1008
  try {
1003
1009
  await player.initialize(containerRef.current, config);
@@ -1524,6 +1530,7 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1524
1530
  props.fallbackShowErrorMessage,
1525
1531
  props.fallbackRetryDelay,
1526
1532
  props.fallbackRetryAttempts,
1533
+ JSON.stringify(props.thumbnailPreview),
1527
1534
  ]);
1528
1535
 
1529
1536
  // Helper function to filter quality levels based on qualityFilter prop
@@ -1765,6 +1772,18 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1765
1772
  }
1766
1773
  }, [JSON.stringify(props.flashNewsTicker)]);
1767
1774
 
1775
+ // Update thumbnail preview when config changes
1776
+ useEffect(() => {
1777
+ const p = playerRef.current as any;
1778
+ if (p && typeof p.setThumbnailPreview === 'function') {
1779
+ try {
1780
+ p.setThumbnailPreview(props.thumbnailPreview || null);
1781
+ } catch (err) {
1782
+ console.warn('[WebPlayerView] Failed to update thumbnail preview:', err);
1783
+ }
1784
+ }
1785
+ }, [JSON.stringify(props.thumbnailPreview)]);
1786
+
1768
1787
  const responsiveStyle = getResponsiveDimensions();
1769
1788
 
1770
1789
  // Prepare EPG config with action handlers
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Thumbnail Preview Types
3
+ * Types for seekbar thumbnail preview functionality
4
+ */
5
+
6
+ /**
7
+ * Generation image mapping format
8
+ * Key: Image URL, Value: "startTime-endTime" in seconds
9
+ * Example: { "https://cdn.example.com/thumb1.jpg": "0-30" }
10
+ */
11
+ export interface ThumbnailGenerationImage {
12
+ [imageUrl: string]: string; // "startTime-endTime"
13
+ }
14
+
15
+ /**
16
+ * Thumbnail preview configuration
17
+ */
18
+ export interface ThumbnailPreviewConfig {
19
+ /** Enable/disable thumbnail preview (default: true if generationImage provided) */
20
+ enabled?: boolean;
21
+ /** Image URL to time range mapping */
22
+ generationImage?: ThumbnailGenerationImage;
23
+ /** Custom styling options */
24
+ style?: {
25
+ /** Width of thumbnail in pixels (default: 160) */
26
+ width?: number;
27
+ /** Height of thumbnail in pixels (default: 90) */
28
+ height?: number;
29
+ /** Border radius in pixels (default: 8) */
30
+ borderRadius?: number;
31
+ };
32
+ /** Preload all thumbnail images on initialization (default: true) */
33
+ preloadImages?: boolean;
34
+ /** Show time display inside thumbnail (default: true) */
35
+ showTimeInThumbnail?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Internal thumbnail entry after transformation
40
+ */
41
+ export interface ThumbnailEntry {
42
+ /** Image URL */
43
+ url: string;
44
+ /** Start time in seconds */
45
+ startTime: number;
46
+ /** End time in seconds */
47
+ endTime: number;
48
+ }
49
+
50
+ /**
51
+ * Thumbnail preview state
52
+ */
53
+ export interface ThumbnailPreviewState {
54
+ /** Whether thumbnail preview is currently visible */
55
+ isVisible: boolean;
56
+ /** Current thumbnail URL being displayed */
57
+ currentUrl: string | null;
58
+ /** Current time position being previewed */
59
+ currentTime: number;
60
+ /** X position of thumbnail on screen */
61
+ xPosition: number;
62
+ }