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.
- 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 +347 -7
- 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 +432 -4
- 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
|
}
|
|
@@ -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
|
+
}
|