unified-video-framework 1.4.445 → 1.4.447

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 (108) 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 +308 -6
  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/drm/BunnyCDNDRMProvider.d.ts +13 -0
  16. package/packages/web/dist/drm/BunnyCDNDRMProvider.d.ts.map +1 -0
  17. package/packages/web/dist/drm/BunnyCDNDRMProvider.js +65 -0
  18. package/packages/web/dist/drm/BunnyCDNDRMProvider.js.map +1 -0
  19. package/packages/web/dist/drm/DRMManager.d.ts +20 -0
  20. package/packages/web/dist/drm/DRMManager.d.ts.map +1 -0
  21. package/packages/web/dist/drm/DRMManager.js +130 -0
  22. package/packages/web/dist/drm/DRMManager.js.map +1 -0
  23. package/packages/web/dist/drm/FairPlayDRMHandler.d.ts +24 -0
  24. package/packages/web/dist/drm/FairPlayDRMHandler.d.ts.map +1 -0
  25. package/packages/web/dist/drm/FairPlayDRMHandler.js +190 -0
  26. package/packages/web/dist/drm/FairPlayDRMHandler.js.map +1 -0
  27. package/packages/web/dist/drm/WidevineDRMHandler.d.ts +21 -0
  28. package/packages/web/dist/drm/WidevineDRMHandler.d.ts.map +1 -0
  29. package/packages/web/dist/drm/WidevineDRMHandler.js +143 -0
  30. package/packages/web/dist/drm/WidevineDRMHandler.js.map +1 -0
  31. package/packages/web/dist/drm/index.d.ts +15 -0
  32. package/packages/web/dist/drm/index.d.ts.map +1 -0
  33. package/packages/web/dist/drm/index.js +13 -0
  34. package/packages/web/dist/drm/index.js.map +1 -0
  35. package/packages/web/dist/drm/providers/BunnyNetProvider.d.ts +19 -0
  36. package/packages/web/dist/drm/providers/BunnyNetProvider.d.ts.map +1 -0
  37. package/packages/web/dist/drm/providers/BunnyNetProvider.js +112 -0
  38. package/packages/web/dist/drm/providers/BunnyNetProvider.js.map +1 -0
  39. package/packages/web/dist/drm/providers/GenericProvider.d.ts +30 -0
  40. package/packages/web/dist/drm/providers/GenericProvider.d.ts.map +1 -0
  41. package/packages/web/dist/drm/providers/GenericProvider.js +104 -0
  42. package/packages/web/dist/drm/providers/GenericProvider.js.map +1 -0
  43. package/packages/web/dist/drm/systems/BaseDRM.d.ts +18 -0
  44. package/packages/web/dist/drm/systems/BaseDRM.d.ts.map +1 -0
  45. package/packages/web/dist/drm/systems/BaseDRM.js +29 -0
  46. package/packages/web/dist/drm/systems/BaseDRM.js.map +1 -0
  47. package/packages/web/dist/drm/systems/FairPlayDRM.d.ts +32 -0
  48. package/packages/web/dist/drm/systems/FairPlayDRM.d.ts.map +1 -0
  49. package/packages/web/dist/drm/systems/FairPlayDRM.js +208 -0
  50. package/packages/web/dist/drm/systems/FairPlayDRM.js.map +1 -0
  51. package/packages/web/dist/drm/systems/PlayReadyDRM.d.ts +9 -0
  52. package/packages/web/dist/drm/systems/PlayReadyDRM.d.ts.map +1 -0
  53. package/packages/web/dist/drm/systems/PlayReadyDRM.js +114 -0
  54. package/packages/web/dist/drm/systems/PlayReadyDRM.js.map +1 -0
  55. package/packages/web/dist/drm/systems/WidevineDRM.d.ts +9 -0
  56. package/packages/web/dist/drm/systems/WidevineDRM.d.ts.map +1 -0
  57. package/packages/web/dist/drm/systems/WidevineDRM.js +159 -0
  58. package/packages/web/dist/drm/systems/WidevineDRM.js.map +1 -0
  59. package/packages/web/dist/drm/types/BunnyNetTypes.d.ts +20 -0
  60. package/packages/web/dist/drm/types/BunnyNetTypes.d.ts.map +1 -0
  61. package/packages/web/dist/drm/types/BunnyNetTypes.js +8 -0
  62. package/packages/web/dist/drm/types/BunnyNetTypes.js.map +1 -0
  63. package/packages/web/dist/drm/types/DRMTypes.d.ts +60 -0
  64. package/packages/web/dist/drm/types/DRMTypes.d.ts.map +1 -0
  65. package/packages/web/dist/drm/types/DRMTypes.js +22 -0
  66. package/packages/web/dist/drm/types/DRMTypes.js.map +1 -0
  67. package/packages/web/dist/drm/utils/BrowserDetector.d.ts +20 -0
  68. package/packages/web/dist/drm/utils/BrowserDetector.d.ts.map +1 -0
  69. package/packages/web/dist/drm/utils/BrowserDetector.js +211 -0
  70. package/packages/web/dist/drm/utils/BrowserDetector.js.map +1 -0
  71. package/packages/web/dist/drm/utils/CertificateManager.d.ts +15 -0
  72. package/packages/web/dist/drm/utils/CertificateManager.d.ts.map +1 -0
  73. package/packages/web/dist/drm/utils/CertificateManager.js +46 -0
  74. package/packages/web/dist/drm/utils/CertificateManager.js.map +1 -0
  75. package/packages/web/dist/drm/utils/DRMErrorHandler.d.ts +7 -0
  76. package/packages/web/dist/drm/utils/DRMErrorHandler.d.ts.map +1 -0
  77. package/packages/web/dist/drm/utils/DRMErrorHandler.js +49 -0
  78. package/packages/web/dist/drm/utils/DRMErrorHandler.js.map +1 -0
  79. package/packages/web/dist/drm/utils/LicenseRequestHandler.d.ts +15 -0
  80. package/packages/web/dist/drm/utils/LicenseRequestHandler.d.ts.map +1 -0
  81. package/packages/web/dist/drm/utils/LicenseRequestHandler.js +110 -0
  82. package/packages/web/dist/drm/utils/LicenseRequestHandler.js.map +1 -0
  83. package/packages/web/dist/drm.js +5 -0
  84. package/packages/web/dist/index.js +4 -4
  85. package/packages/web/dist/paywall/PaywallController.js +1 -1
  86. package/packages/web/dist/react/EPG.js +6 -6
  87. package/packages/web/dist/react/WebPlayerView.d.ts +399 -0
  88. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  89. package/packages/web/dist/react/WebPlayerView.js +19 -6
  90. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  91. package/packages/web/dist/react/WebPlayerViewWithEPG.js +2 -2
  92. package/packages/web/dist/react/components/ChapterProgress.js +1 -1
  93. package/packages/web/dist/react/components/EPGOverlay-improved-positioning.js +5 -5
  94. package/packages/web/dist/react/components/EPGOverlay.js +5 -5
  95. package/packages/web/dist/react/components/EPGProgramDetails.js +1 -1
  96. package/packages/web/dist/react/components/EPGProgramGrid.js +1 -1
  97. package/packages/web/dist/react/components/EPGTimelineHeader.js +1 -1
  98. package/packages/web/dist/react/components/SkipButton.js +1 -1
  99. package/packages/web/dist/react/examples/google-ads-example.js +1 -1
  100. package/packages/web/dist/react/types/ThumbnailPreviewTypes.d.ts +26 -0
  101. package/packages/web/dist/react/types/ThumbnailPreviewTypes.d.ts.map +1 -0
  102. package/packages/web/dist/react/types/ThumbnailPreviewTypes.js +2 -0
  103. package/packages/web/dist/react/types/ThumbnailPreviewTypes.js.map +1 -0
  104. package/packages/web/dist/test/epg-test.js +1 -1
  105. package/packages/web/src/WebPlayer.ts +389 -3
  106. package/packages/web/src/react/WebPlayerView.tsx +21 -2
  107. package/packages/web/src/react/types/ThumbnailPreviewTypes.ts +62 -0
  108. package/scripts/fix-imports.js +61 -23
@@ -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
  }
@@ -5506,7 +5760,122 @@ export class WebPlayer extends BasePlayer {
5506
5760
  opacity: 1;
5507
5761
  transform: translateX(-50%) translateY(0);
5508
5762
  }
5509
-
5763
+
5764
+ /* Thumbnail Preview */
5765
+ .uvf-thumbnail-preview {
5766
+ position: absolute;
5767
+ bottom: 100%;
5768
+ left: 0;
5769
+ margin-bottom: 12px;
5770
+ display: flex;
5771
+ flex-direction: column;
5772
+ align-items: center;
5773
+ pointer-events: none;
5774
+ z-index: 25;
5775
+ opacity: 0;
5776
+ transform: translateX(-50%) translateY(8px);
5777
+ transition: opacity 0.15s ease, transform 0.15s ease;
5778
+ }
5779
+
5780
+ .uvf-thumbnail-preview.visible {
5781
+ opacity: 1;
5782
+ transform: translateX(-50%) translateY(0);
5783
+ }
5784
+
5785
+ .uvf-thumbnail-preview-container {
5786
+ position: relative;
5787
+ background: rgba(0, 0, 0, 0.9);
5788
+ border-radius: 8px;
5789
+ padding: 4px;
5790
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.1);
5791
+ backdrop-filter: blur(12px);
5792
+ -webkit-backdrop-filter: blur(12px);
5793
+ }
5794
+
5795
+ .uvf-thumbnail-preview-image-wrapper {
5796
+ position: relative;
5797
+ width: 160px;
5798
+ height: 90px;
5799
+ border-radius: 6px;
5800
+ overflow: hidden;
5801
+ background: rgba(30, 30, 30, 0.95);
5802
+ }
5803
+
5804
+ .uvf-thumbnail-preview-image {
5805
+ width: 100%;
5806
+ height: 100%;
5807
+ object-fit: cover;
5808
+ display: block;
5809
+ opacity: 0;
5810
+ transition: opacity 0.2s ease;
5811
+ }
5812
+
5813
+ .uvf-thumbnail-preview-image.loaded {
5814
+ opacity: 1;
5815
+ }
5816
+
5817
+ .uvf-thumbnail-preview-loading {
5818
+ position: absolute;
5819
+ top: 50%;
5820
+ left: 50%;
5821
+ transform: translate(-50%, -50%);
5822
+ width: 24px;
5823
+ height: 24px;
5824
+ border: 2px solid rgba(255, 255, 255, 0.2);
5825
+ border-top-color: var(--uvf-accent-1, #ff5722);
5826
+ border-radius: 50%;
5827
+ animation: uvf-spin 0.8s linear infinite;
5828
+ }
5829
+
5830
+ .uvf-thumbnail-preview-image.loaded + .uvf-thumbnail-preview-loading {
5831
+ display: none;
5832
+ }
5833
+
5834
+ .uvf-thumbnail-preview-time {
5835
+ position: absolute;
5836
+ bottom: 6px;
5837
+ left: 50%;
5838
+ transform: translateX(-50%);
5839
+ background: rgba(0, 0, 0, 0.85);
5840
+ color: #fff;
5841
+ font-size: 12px;
5842
+ font-weight: 600;
5843
+ padding: 3px 8px;
5844
+ border-radius: 4px;
5845
+ white-space: nowrap;
5846
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
5847
+ backdrop-filter: blur(4px);
5848
+ -webkit-backdrop-filter: blur(4px);
5849
+ }
5850
+
5851
+ .uvf-thumbnail-preview-arrow {
5852
+ width: 0;
5853
+ height: 0;
5854
+ border-left: 8px solid transparent;
5855
+ border-right: 8px solid transparent;
5856
+ border-top: 8px solid rgba(0, 0, 0, 0.9);
5857
+ margin-top: -1px;
5858
+ }
5859
+
5860
+ /* Hide regular time tooltip when thumbnail preview is visible */
5861
+ .uvf-thumbnail-preview.visible ~ .uvf-time-tooltip {
5862
+ opacity: 0 !important;
5863
+ pointer-events: none;
5864
+ }
5865
+
5866
+ /* Responsive thumbnail sizes */
5867
+ @media (max-width: 768px) {
5868
+ .uvf-thumbnail-preview-image-wrapper {
5869
+ width: 120px;
5870
+ height: 68px;
5871
+ }
5872
+
5873
+ .uvf-thumbnail-preview-time {
5874
+ font-size: 11px;
5875
+ padding: 2px 6px;
5876
+ }
5877
+ }
5878
+
5510
5879
  /* Chapter Markers */
5511
5880
  .uvf-chapter-marker {
5512
5881
  position: absolute;
@@ -8602,6 +8971,16 @@ export class WebPlayer extends BasePlayer {
8602
8971
  <div class="uvf-progress-filled" id="uvf-progress-filled"></div>
8603
8972
  <div class="uvf-progress-handle" id="uvf-progress-handle"></div>
8604
8973
  </div>
8974
+ <div class="uvf-thumbnail-preview" id="uvf-thumbnail-preview">
8975
+ <div class="uvf-thumbnail-preview-container">
8976
+ <div class="uvf-thumbnail-preview-image-wrapper">
8977
+ <img class="uvf-thumbnail-preview-image" id="uvf-thumbnail-image" alt="Preview" />
8978
+ <div class="uvf-thumbnail-preview-loading"></div>
8979
+ </div>
8980
+ <div class="uvf-thumbnail-preview-time" id="uvf-thumbnail-time">00:00</div>
8981
+ </div>
8982
+ <div class="uvf-thumbnail-preview-arrow"></div>
8983
+ </div>
8605
8984
  <div class="uvf-time-tooltip" id="uvf-time-tooltip">00:00</div>
8606
8985
  `;
8607
8986
  progressSection.appendChild(progressBar);
@@ -9010,12 +9389,15 @@ export class WebPlayer extends BasePlayer {
9010
9389
  if (!this.isDragging) {
9011
9390
  this.showTimeTooltip = false;
9012
9391
  this.hideTimeTooltip();
9392
+ this.hideThumbnailPreview();
9013
9393
  }
9014
9394
  });
9015
9395
 
9016
9396
  progressBar?.addEventListener('mousemove', (e) => {
9017
9397
  if (this.showTimeTooltip) {
9018
9398
  this.updateTimeTooltip(e as MouseEvent);
9399
+ // Update thumbnail preview on hover
9400
+ this.updateThumbnailPreview(e as MouseEvent);
9019
9401
  }
9020
9402
  });
9021
9403
 
@@ -9040,6 +9422,8 @@ export class WebPlayer extends BasePlayer {
9040
9422
  this.seekToPosition(e);
9041
9423
  // Update tooltip position during dragging
9042
9424
  this.updateTimeTooltip(e);
9425
+ // Update thumbnail preview during dragging
9426
+ this.updateThumbnailPreview(e);
9043
9427
  }
9044
9428
  });
9045
9429
 
@@ -9070,10 +9454,11 @@ export class WebPlayer extends BasePlayer {
9070
9454
  // Remove dragging class from handle
9071
9455
  const handle = document.getElementById('uvf-progress-handle');
9072
9456
  handle?.classList.remove('dragging');
9073
- // Hide tooltip if mouse is not over progress bar
9457
+ // Hide tooltip and thumbnail preview if mouse is not over progress bar
9074
9458
  if (progressBar && !progressBar.matches(':hover')) {
9075
9459
  this.showTimeTooltip = false;
9076
9460
  this.hideTimeTooltip();
9461
+ this.hideThumbnailPreview();
9077
9462
  }
9078
9463
  }
9079
9464
  });
@@ -9084,9 +9469,10 @@ export class WebPlayer extends BasePlayer {
9084
9469
  // Remove dragging class from handle
9085
9470
  const handle = document.getElementById('uvf-progress-handle');
9086
9471
  handle?.classList.remove('dragging');
9087
- // Hide tooltip on touch end
9472
+ // Hide tooltip and thumbnail preview on touch end
9088
9473
  this.showTimeTooltip = false;
9089
9474
  this.hideTimeTooltip();
9475
+ this.hideThumbnailPreview();
9090
9476
  }
9091
9477
  });
9092
9478
 
@@ -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
+ }
@@ -29,19 +29,48 @@ function fixImports(filePath) {
29
29
  );
30
30
 
31
31
  // Fix relative imports within the same package to include .js extension
32
- content = content.replace(
33
- /from\s+["']\.\/([^"']+)(?<!\.js)["']/g,
34
- 'from "./$1.js"'
35
- );
36
- content = content.replace(
37
- /from\s+["']\.\.?\/([^"']+)(?<!\.js)["']/g,
38
- (match, p1) => {
39
- if (p1.includes('/')) {
40
- return `from "../${p1.replace(/([^\/]+)$/, '$1.js')}"`;
32
+ // Handle both files (append .js) and directories (append /index.js)
33
+ const fixRelativeImport = (match, p1) => {
34
+ const importPath = path.resolve(path.dirname(filePath), p1);
35
+
36
+ // If it already ends with .js, check if it's actually a directory that was incorrectly named
37
+ if (p1.endsWith('.js')) {
38
+ const dirPath = importPath.substring(0, importPath.length - 3);
39
+ if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
40
+ console.log(` Fixing incorrect .js to /index.js: ${p1} -> ${p1.substring(0, p1.length - 3)}/index.js`);
41
+ return match.replace(p1, `${p1.substring(0, p1.length - 3)}/index.js`);
41
42
  }
42
- return `from "../${p1}.js"`;
43
+ return match;
43
44
  }
44
- );
45
+
46
+ try {
47
+ if (fs.existsSync(importPath) && fs.statSync(importPath).isDirectory()) {
48
+ console.log(` Directory found: ${p1} -> ${p1}/index.js`);
49
+ return match.replace(p1, `${p1}/index.js`);
50
+ }
51
+ } catch (e) {
52
+ // If we can't check, assume it's a file
53
+ }
54
+
55
+ console.log(` File assumed: ${p1} -> ${p1}.js`);
56
+ return match.replace(p1, `${p1}.js`);
57
+ };
58
+
59
+ content = content.replace(/from\s+["'](\.\.?\/[^"']+)["']/g, fixRelativeImport);
60
+
61
+ // Also fix import statements that don't use 'from'
62
+ content = content.replace(/import\s+["'](\.\.?\/[^"']+)["']/g, (match, p1) => {
63
+ if (p1.endsWith('.js')) return match;
64
+ const importPath = path.resolve(path.dirname(filePath), p1);
65
+ try {
66
+ if (fs.existsSync(importPath) && fs.statSync(importPath).isDirectory()) {
67
+ console.log(` Import Dir found: ${p1} -> ${p1}/index.js`);
68
+ return match.replace(p1, `${p1}/index.js`);
69
+ }
70
+ } catch (e) { }
71
+ console.log(` Import File assumed: ${p1} -> ${p1}.js`);
72
+ return match.replace(p1, `${p1}.js`);
73
+ });
45
74
  } else if (filePath.includes(path.join('packages', 'react-native', 'dist'))) {
46
75
  // Fix CommonJS require statements
47
76
  content = content.replace(
@@ -59,19 +88,28 @@ function fixImports(filePath) {
59
88
  );
60
89
 
61
90
  // Fix relative imports within the same package to include .js extension
62
- content = content.replace(
63
- /from\s+["']\.\/([^"']+)(?<!\.js)["']/g,
64
- 'from "./$1.js"'
65
- );
66
- content = content.replace(
67
- /from\s+["']\.\.?\/([^"']+)(?<!\.js)["']/g,
68
- (match, p1) => {
69
- if (p1.includes('/')) {
70
- return `from "../${p1.replace(/([^\/]+)$/, '$1.js')}"`;
91
+ const fixRelativeImport = (match, p1) => {
92
+ if (p1.endsWith('.js')) return `from "${p1}"`;
93
+ const importPath = path.resolve(path.dirname(filePath), p1);
94
+ try {
95
+ if (fs.existsSync(importPath) && fs.statSync(importPath).isDirectory()) {
96
+ return `from "${p1}/index.js"`;
71
97
  }
72
- return `from "../${p1}.js"`;
73
- }
74
- );
98
+ } catch (e) { }
99
+ return `from "${p1}.js"`;
100
+ };
101
+
102
+ content = content.replace(/from\s+["'](\.\.?\/[^"']+)["']/g, fixRelativeImport);
103
+ content = content.replace(/import\s+["'](\.\.?\/[^"']+)["']/g, (match, p1) => {
104
+ if (p1.endsWith('.js')) return match;
105
+ const importPath = path.resolve(path.dirname(filePath), p1);
106
+ try {
107
+ if (fs.existsSync(importPath) && fs.statSync(importPath).isDirectory()) {
108
+ return `import "${p1}/index.js"`;
109
+ }
110
+ } catch (e) { }
111
+ return `import "${p1}.js"`;
112
+ });
75
113
  }
76
114
 
77
115
  fs.writeFileSync(filePath, content, 'utf8');