unified-video-framework 1.4.446 → 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.
- package/package.json +1 -1
- package/packages/core/dist/interfaces.d.ts +14 -0
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/dist/version.d.ts +1 -1
- package/packages/core/dist/version.js +1 -1
- package/packages/core/src/interfaces.ts +24 -0
- package/packages/core/src/version.ts +1 -1
- package/packages/web/dist/WebPlayer.d.ts +364 -0
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +308 -6
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.js +3 -3
- package/packages/web/dist/chapters/SkipButtonController.js +1 -1
- package/packages/web/dist/chapters/index.js +7 -7
- package/packages/web/dist/index.js +4 -4
- package/packages/web/dist/paywall/PaywallController.js +1 -1
- package/packages/web/dist/react/EPG.js +6 -6
- package/packages/web/dist/react/WebPlayerView.d.ts +399 -0
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +19 -6
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/dist/react/WebPlayerViewWithEPG.js +2 -2
- package/packages/web/dist/react/components/ChapterProgress.js +1 -1
- package/packages/web/dist/react/components/EPGOverlay-improved-positioning.js +5 -5
- package/packages/web/dist/react/components/EPGOverlay.js +5 -5
- package/packages/web/dist/react/components/EPGProgramDetails.js +1 -1
- package/packages/web/dist/react/components/EPGProgramGrid.js +1 -1
- package/packages/web/dist/react/components/EPGTimelineHeader.js +1 -1
- package/packages/web/dist/react/components/SkipButton.js +1 -1
- package/packages/web/dist/react/examples/google-ads-example.js +1 -1
- package/packages/web/dist/react/types/ThumbnailPreviewTypes.d.ts +26 -0
- package/packages/web/dist/react/types/ThumbnailPreviewTypes.d.ts.map +1 -0
- package/packages/web/dist/react/types/ThumbnailPreviewTypes.js +2 -0
- package/packages/web/dist/react/types/ThumbnailPreviewTypes.js.map +1 -0
- package/packages/web/dist/test/epg-test.js +1 -1
- package/packages/web/src/WebPlayer.ts +389 -3
- package/packages/web/src/react/WebPlayerView.tsx +21 -2
- 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
|
}
|
|
@@ -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
|
+
}
|