unified-video-framework 1.4.437 → 1.4.439

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.
@@ -34,6 +34,17 @@ export class WebPlayer extends BasePlayer {
34
34
  this.flashTickerContainer = null;
35
35
  this.flashTickerTopElement = null;
36
36
  this.flashTickerBottomElement = null;
37
+ this.tickerCurrentItemIndex = 0;
38
+ this.tickerCycleTimer = null;
39
+ this.tickerConfig = null;
40
+ this.tickerHeadlineElement = null;
41
+ this.tickerDetailElement = null;
42
+ this.tickerIntroOverlay = null;
43
+ this.tickerProgressBar = null;
44
+ this.tickerProgressFill = null;
45
+ this.tickerIsPaused = false;
46
+ this.tickerPauseStartTime = 0;
47
+ this.tickerRemainingTime = 0;
37
48
  this.previewGateHit = false;
38
49
  this.paymentSuccessTime = 0;
39
50
  this.paymentSuccessful = false;
@@ -97,6 +108,8 @@ export class WebPlayer extends BasePlayer {
97
108
  this.currentRetryAttempt = 0;
98
109
  this.lastFailedUrl = '';
99
110
  this.isFallbackPosterMode = false;
111
+ this.hlsErrorRetryCount = 0;
112
+ this.MAX_HLS_ERROR_RETRIES = 3;
100
113
  this.lastDuration = 0;
101
114
  this.isDetectedAsLive = false;
102
115
  this.autoplayAttempted = false;
@@ -531,6 +544,7 @@ export class WebPlayer extends BasePlayer {
531
544
  this.currentRetryAttempt = 0;
532
545
  this.lastFailedUrl = '';
533
546
  this.isFallbackPosterMode = false;
547
+ this.hlsErrorRetryCount = 0;
534
548
  await this.cleanup();
535
549
  if (!this.video) {
536
550
  throw new Error('Video element not initialized');
@@ -828,6 +842,7 @@ export class WebPlayer extends BasePlayer {
828
842
  this.hls.loadSource(url);
829
843
  this.hls.attachMedia(this.video);
830
844
  this.hls.on(window.Hls.Events.MANIFEST_PARSED, (event, data) => {
845
+ this.hlsErrorRetryCount = 0;
831
846
  this.qualities = data.levels.map((level, index) => ({
832
847
  height: level.height,
833
848
  width: level.width || 0,
@@ -861,27 +876,85 @@ export class WebPlayer extends BasePlayer {
861
876
  throw new Error('HLS is not supported in this browser');
862
877
  }
863
878
  }
864
- handleHLSError(data) {
879
+ async handleHLSError(data) {
865
880
  const Hls = window.Hls;
881
+ this.debugLog(`🔴 HLS Error: type=${data.type}, details=${data.details}, fatal=${data.fatal}`);
882
+ if (this.hlsErrorRetryCount >= this.MAX_HLS_ERROR_RETRIES) {
883
+ this.debugLog(`🔴 HLS max retries (${this.MAX_HLS_ERROR_RETRIES}) exceeded, triggering fallback`);
884
+ this.hls?.destroy();
885
+ this.hls = null;
886
+ const fallbackLoaded = await this.tryFallbackSource({
887
+ code: 'HLS_ERROR',
888
+ message: data.details,
889
+ type: data.type,
890
+ fatal: true,
891
+ details: data
892
+ });
893
+ if (!fallbackLoaded) {
894
+ this.handleError({
895
+ code: 'HLS_ERROR',
896
+ message: `HLS stream failed after ${this.MAX_HLS_ERROR_RETRIES} retries: ${data.details}`,
897
+ type: 'media',
898
+ fatal: true,
899
+ details: data
900
+ });
901
+ }
902
+ return;
903
+ }
866
904
  switch (data.type) {
867
905
  case Hls.ErrorTypes.NETWORK_ERROR:
868
- console.error('Fatal network error, trying to recover');
869
- this.hls.startLoad();
906
+ this.hlsErrorRetryCount++;
907
+ this.debugLog(`🔴 Fatal network error (attempt ${this.hlsErrorRetryCount}/${this.MAX_HLS_ERROR_RETRIES}), trying to recover`);
908
+ if (data.details === 'manifestLoadError' || data.details === 'manifestParsingError') {
909
+ this.debugLog(`🔴 Manifest error detected (${data.details}), skipping recovery - triggering fallback`);
910
+ this.hls?.destroy();
911
+ this.hls = null;
912
+ const fallbackLoaded = await this.tryFallbackSource({
913
+ code: 'HLS_MANIFEST_ERROR',
914
+ message: data.details,
915
+ type: 'network',
916
+ fatal: true,
917
+ details: data
918
+ });
919
+ if (!fallbackLoaded) {
920
+ this.handleError({
921
+ code: 'HLS_MANIFEST_ERROR',
922
+ message: `Failed to load HLS manifest: ${data.details}`,
923
+ type: 'media',
924
+ fatal: true,
925
+ details: data
926
+ });
927
+ }
928
+ }
929
+ else {
930
+ this.hls?.startLoad();
931
+ }
870
932
  break;
871
933
  case Hls.ErrorTypes.MEDIA_ERROR:
872
- console.error('Fatal media error, trying to recover');
873
- this.hls.recoverMediaError();
934
+ this.hlsErrorRetryCount++;
935
+ this.debugLog(`🔴 Fatal media error (attempt ${this.hlsErrorRetryCount}/${this.MAX_HLS_ERROR_RETRIES}), trying to recover`);
936
+ this.hls?.recoverMediaError();
874
937
  break;
875
938
  default:
876
- console.error('Fatal error, cannot recover');
877
- this.handleError({
939
+ this.debugLog(`🔴 Fatal unrecoverable HLS error: ${data.details}`);
940
+ this.hls?.destroy();
941
+ this.hls = null;
942
+ const fallbackLoaded = await this.tryFallbackSource({
878
943
  code: 'HLS_ERROR',
879
944
  message: data.details,
880
945
  type: 'media',
881
946
  fatal: true,
882
947
  details: data
883
948
  });
884
- this.hls.destroy();
949
+ if (!fallbackLoaded) {
950
+ this.handleError({
951
+ code: 'HLS_ERROR',
952
+ message: data.details,
953
+ type: 'media',
954
+ fatal: true,
955
+ details: data
956
+ });
957
+ }
885
958
  break;
886
959
  }
887
960
  }
@@ -1877,6 +1950,10 @@ export class WebPlayer extends BasePlayer {
1877
1950
  }
1878
1951
  createTickerElement(config, position) {
1879
1952
  if (config.styleVariant === 'broadcast') {
1953
+ const layoutStyle = config.broadcastStyle?.layoutStyle || 'broadcast';
1954
+ if (layoutStyle === 'two-line' || layoutStyle === 'professional') {
1955
+ return this.createProfessionalTickerElement(config, position);
1956
+ }
1880
1957
  return this.createBroadcastTickerElement(config, position);
1881
1958
  }
1882
1959
  return this.createSimpleTickerElement(config, position);
@@ -2179,14 +2256,19 @@ export class WebPlayer extends BasePlayer {
2179
2256
  const style = document.createElement('style');
2180
2257
  style.id = 'uvf-ticker-animation';
2181
2258
  style.textContent = `
2259
+ /* Basic ticker scroll */
2182
2260
  @keyframes ticker-scroll {
2183
2261
  0% { transform: translateX(0%); }
2184
2262
  100% { transform: translateX(-100%); }
2185
2263
  }
2264
+
2265
+ /* Globe rotation */
2186
2266
  @keyframes globe-rotate {
2187
2267
  0% { transform: rotate(0deg); }
2188
2268
  100% { transform: rotate(360deg); }
2189
2269
  }
2270
+
2271
+ /* LIVE badge animations */
2190
2272
  @keyframes live-pulse {
2191
2273
  0%, 100% { opacity: 1; transform: scale(1); }
2192
2274
  50% { opacity: 0.85; transform: scale(1.02); }
@@ -2195,6 +2277,85 @@ export class WebPlayer extends BasePlayer {
2195
2277
  0%, 100% { opacity: 1; }
2196
2278
  50% { opacity: 0.3; }
2197
2279
  }
2280
+
2281
+ /* Intro Animations */
2282
+ @keyframes intro-slide-in {
2283
+ 0% { transform: translateX(-100%); opacity: 0; }
2284
+ 20% { transform: translateX(0); opacity: 1; }
2285
+ 80% { transform: translateX(0); opacity: 1; }
2286
+ 100% { transform: translateX(100%); opacity: 0; }
2287
+ }
2288
+
2289
+ @keyframes intro-flash {
2290
+ 0%, 100% { opacity: 0; }
2291
+ 10%, 30%, 50%, 70%, 90% { opacity: 1; background: rgba(255,255,255,0.3); }
2292
+ 20%, 40%, 60%, 80% { opacity: 1; background: transparent; }
2293
+ }
2294
+
2295
+ @keyframes intro-scale {
2296
+ 0% { transform: scale(0); opacity: 0; }
2297
+ 20% { transform: scale(1.2); opacity: 1; }
2298
+ 30% { transform: scale(1); }
2299
+ 80% { transform: scale(1); opacity: 1; }
2300
+ 100% { transform: scale(0); opacity: 0; }
2301
+ }
2302
+
2303
+ @keyframes intro-pulse {
2304
+ 0% { transform: scale(0.8); opacity: 0; }
2305
+ 25% { transform: scale(1.1); opacity: 1; }
2306
+ 50% { transform: scale(1); opacity: 1; }
2307
+ 75% { transform: scale(1); opacity: 1; }
2308
+ 100% { transform: scale(0.8); opacity: 0; }
2309
+ }
2310
+
2311
+ @keyframes intro-shake {
2312
+ 0% { transform: translateX(0); opacity: 0; }
2313
+ 10% { opacity: 1; }
2314
+ 15%, 35%, 55%, 75% { transform: translateX(-5px); }
2315
+ 25%, 45%, 65%, 85% { transform: translateX(5px); }
2316
+ 90% { transform: translateX(0); opacity: 1; }
2317
+ 100% { transform: translateX(0); opacity: 0; }
2318
+ }
2319
+
2320
+ @keyframes intro-none {
2321
+ 0% { opacity: 1; }
2322
+ 80% { opacity: 1; }
2323
+ 100% { opacity: 0; }
2324
+ }
2325
+
2326
+ /* Item Transitions */
2327
+ @keyframes item-fade-out {
2328
+ from { opacity: 1; }
2329
+ to { opacity: 0; }
2330
+ }
2331
+ @keyframes item-fade-in {
2332
+ from { opacity: 0; }
2333
+ to { opacity: 1; }
2334
+ }
2335
+ @keyframes item-slide-out {
2336
+ from { transform: translateY(0); opacity: 1; }
2337
+ to { transform: translateY(-100%); opacity: 0; }
2338
+ }
2339
+ @keyframes item-slide-in {
2340
+ from { transform: translateY(100%); opacity: 0; }
2341
+ to { transform: translateY(0); opacity: 1; }
2342
+ }
2343
+
2344
+ /* Decorative Separator Animations */
2345
+ @keyframes separator-pulse {
2346
+ 0%, 100% { opacity: 0.8; transform: scaleY(1); }
2347
+ 50% { opacity: 1; transform: scaleY(1.1); }
2348
+ }
2349
+
2350
+ @keyframes chevron-pulse {
2351
+ 0%, 100% { transform: translateX(0); opacity: 0.8; }
2352
+ 50% { transform: translateX(3px); opacity: 1; }
2353
+ }
2354
+
2355
+ @keyframes diamond-spin {
2356
+ from { transform: rotate(0deg); }
2357
+ to { transform: rotate(360deg); }
2358
+ }
2198
2359
  `;
2199
2360
  document.head.appendChild(style);
2200
2361
  }
@@ -2211,6 +2372,516 @@ export class WebPlayer extends BasePlayer {
2211
2372
  const totalWidth = (itemsLength * avgCharWidth) + (config.items.length * gap) + (config.items.length * separatorWidth);
2212
2373
  return Math.max(totalWidth / speed, 15);
2213
2374
  }
2375
+ createProfessionalTickerElement(config, position) {
2376
+ const broadcastStyle = config.broadcastStyle || {};
2377
+ const theme = broadcastStyle.theme || 'breaking-red';
2378
+ const twoLineConfig = broadcastStyle.twoLineDisplay || {};
2379
+ const itemCycling = config.itemCycling || {};
2380
+ this.tickerConfig = config;
2381
+ this.tickerCurrentItemIndex = 0;
2382
+ const themeColors = this.getBroadcastThemeColors(theme, broadcastStyle);
2383
+ const ticker = document.createElement('div');
2384
+ ticker.className = `uvf-flash-ticker ticker-${position} ticker-professional`;
2385
+ const bottomOffset = config.bottomOffset || 0;
2386
+ const topOffset = config.topOffset || 0;
2387
+ const headerHeight = broadcastStyle.headerHeight || 28;
2388
+ const topLineConfig = twoLineConfig.topLine || {};
2389
+ const bottomLineConfig = twoLineConfig.bottomLine || {};
2390
+ const topLineHeight = topLineConfig.minHeight || 32;
2391
+ const bottomLineHeight = bottomLineConfig.height || 28;
2392
+ const bodyHeight = twoLineConfig.enabled !== false ? topLineHeight + bottomLineHeight : (config.height || 36);
2393
+ const progressHeight = itemCycling.showProgress ? (itemCycling.progressHeight || 3) : 0;
2394
+ const totalHeight = headerHeight + bodyHeight + progressHeight;
2395
+ ticker.style.cssText = `
2396
+ position: absolute;
2397
+ left: 0;
2398
+ right: 0;
2399
+ height: ${totalHeight}px;
2400
+ ${position === 'top' ? `top: ${topOffset}px;` : `bottom: ${bottomOffset}px;`}
2401
+ overflow: hidden;
2402
+ pointer-events: ${itemCycling.pauseOnHover !== false ? 'auto' : 'none'};
2403
+ display: flex;
2404
+ flex-direction: column;
2405
+ `;
2406
+ const header = this.createProfessionalHeader(broadcastStyle, themeColors, headerHeight);
2407
+ ticker.appendChild(header);
2408
+ const body = this.createTwoLineBody(config, twoLineConfig, themeColors.bodyBg, bodyHeight);
2409
+ ticker.appendChild(body);
2410
+ if (itemCycling.showProgress) {
2411
+ const progressBar = this.createProgressBar(itemCycling);
2412
+ ticker.appendChild(progressBar);
2413
+ }
2414
+ const introOverlay = this.createIntroOverlay(broadcastStyle);
2415
+ ticker.appendChild(introOverlay);
2416
+ this.tickerIntroOverlay = introOverlay;
2417
+ if (itemCycling.pauseOnHover !== false && itemCycling.enabled) {
2418
+ ticker.addEventListener('mouseenter', () => this.pauseItemCycling());
2419
+ ticker.addEventListener('mouseleave', () => this.resumeItemCycling());
2420
+ }
2421
+ this.ensureTickerAnimations();
2422
+ if (itemCycling.enabled && config.items && config.items.length > 1) {
2423
+ const firstItem = config.items[0];
2424
+ if (firstItem.showIntro) {
2425
+ this.playIntroAnimation(firstItem).then(() => {
2426
+ this.startItemCycling();
2427
+ });
2428
+ }
2429
+ else {
2430
+ this.startItemCycling();
2431
+ }
2432
+ }
2433
+ return ticker;
2434
+ }
2435
+ createProfessionalHeader(broadcastStyle, themeColors, headerHeight) {
2436
+ const header = document.createElement('div');
2437
+ header.className = 'uvf-ticker-header-row';
2438
+ header.style.cssText = `
2439
+ display: flex;
2440
+ align-items: center;
2441
+ height: ${headerHeight}px;
2442
+ background: ${themeColors.headerBg};
2443
+ padding: 0 12px;
2444
+ position: relative;
2445
+ `;
2446
+ if (broadcastStyle.showGlobe !== false) {
2447
+ const globe = this.createGlobeElement(broadcastStyle.animateGlobe !== false);
2448
+ header.appendChild(globe);
2449
+ }
2450
+ if (broadcastStyle.decorativeShapes?.headerSeparator) {
2451
+ const separator = this.createDecorativeSeparator(broadcastStyle.decorativeShapes.headerSeparator);
2452
+ header.appendChild(separator);
2453
+ }
2454
+ else if (broadcastStyle.showGlobe !== false) {
2455
+ const defaultSeparator = document.createElement('div');
2456
+ defaultSeparator.className = 'uvf-ticker-separator';
2457
+ defaultSeparator.style.cssText = `
2458
+ width: 2px;
2459
+ height: 18px;
2460
+ background: rgba(255,255,255,0.6);
2461
+ margin: 0 10px;
2462
+ `;
2463
+ header.appendChild(defaultSeparator);
2464
+ }
2465
+ const headerText = document.createElement('span');
2466
+ headerText.className = 'uvf-ticker-header-text';
2467
+ headerText.textContent = broadcastStyle.headerText || 'BREAKING NEWS';
2468
+ headerText.style.cssText = `
2469
+ color: ${broadcastStyle.headerTextColor || '#ffffff'};
2470
+ font-size: ${broadcastStyle.headerFontSize || 16}px;
2471
+ font-weight: 800;
2472
+ text-transform: uppercase;
2473
+ letter-spacing: 1px;
2474
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
2475
+ `;
2476
+ header.appendChild(headerText);
2477
+ if (broadcastStyle.showLiveBadge !== false) {
2478
+ const liveBadge = this.createLiveBadgeElement(broadcastStyle.pulseLiveBadge !== false);
2479
+ header.appendChild(liveBadge);
2480
+ }
2481
+ return header;
2482
+ }
2483
+ createDecorativeSeparator(config) {
2484
+ const separator = document.createElement('div');
2485
+ separator.className = 'uvf-ticker-separator';
2486
+ const width = config.width || 2;
2487
+ const height = config.height || 20;
2488
+ const color = config.color || '#ffffff';
2489
+ const animated = config.animated !== false;
2490
+ const type = config.type || 'line';
2491
+ let animationStyle = '';
2492
+ let content = '';
2493
+ switch (type) {
2494
+ case 'pulse-line':
2495
+ animationStyle = animated ? 'animation: separator-pulse 1.5s ease-in-out infinite;' : '';
2496
+ break;
2497
+ case 'chevron':
2498
+ content = '›';
2499
+ animationStyle = animated ? 'animation: chevron-pulse 1s ease-in-out infinite;' : '';
2500
+ break;
2501
+ case 'diamond':
2502
+ content = '◆';
2503
+ animationStyle = animated ? 'animation: diamond-spin 3s linear infinite;' : '';
2504
+ break;
2505
+ case 'dot':
2506
+ content = '•';
2507
+ animationStyle = animated ? 'animation: dot-blink 1s ease-in-out infinite;' : '';
2508
+ break;
2509
+ default:
2510
+ break;
2511
+ }
2512
+ if (type === 'line' || type === 'pulse-line') {
2513
+ separator.style.cssText = `
2514
+ width: ${width}px;
2515
+ height: ${height}px;
2516
+ background: ${color};
2517
+ margin: 0 10px;
2518
+ opacity: 0.8;
2519
+ ${animationStyle}
2520
+ `;
2521
+ }
2522
+ else {
2523
+ separator.textContent = content;
2524
+ separator.style.cssText = `
2525
+ color: ${color};
2526
+ font-size: ${height}px;
2527
+ margin: 0 10px;
2528
+ opacity: 0.8;
2529
+ display: flex;
2530
+ align-items: center;
2531
+ justify-content: center;
2532
+ ${animationStyle}
2533
+ `;
2534
+ }
2535
+ return separator;
2536
+ }
2537
+ createTwoLineBody(config, twoLineConfig, bodyBg, bodyHeight) {
2538
+ const body = document.createElement('div');
2539
+ body.className = 'uvf-ticker-body-row';
2540
+ body.style.cssText = `
2541
+ display: flex;
2542
+ flex-direction: column;
2543
+ height: ${bodyHeight}px;
2544
+ background: ${bodyBg};
2545
+ overflow: hidden;
2546
+ position: relative;
2547
+ `;
2548
+ const topLineConfig = twoLineConfig.topLine || {};
2549
+ const bottomLineConfig = twoLineConfig.bottomLine || {};
2550
+ const firstItem = config.items?.[0];
2551
+ const headlineLine = document.createElement('div');
2552
+ headlineLine.className = 'uvf-ticker-headline-line';
2553
+ this.tickerHeadlineElement = headlineLine;
2554
+ const topLineHeight = topLineConfig.minHeight || 32;
2555
+ const topLineFontSize = topLineConfig.fontSize || 16;
2556
+ const topLineLineHeight = topLineConfig.lineHeight || 1.3;
2557
+ const topLineMultiLine = topLineConfig.multiLine !== false;
2558
+ const topLineMaxLines = topLineConfig.maxLines || 2;
2559
+ headlineLine.style.cssText = `
2560
+ display: flex;
2561
+ align-items: center;
2562
+ min-height: ${topLineHeight}px;
2563
+ padding: ${topLineConfig.padding || 8}px 12px;
2564
+ background: ${topLineConfig.backgroundColor || 'transparent'};
2565
+ overflow: hidden;
2566
+ ${topLineMultiLine ? '' : 'white-space: nowrap;'}
2567
+ `;
2568
+ const headlineText = document.createElement('span');
2569
+ headlineText.className = 'uvf-ticker-headline-text';
2570
+ headlineText.style.cssText = `
2571
+ color: ${topLineConfig.textColor || config.textColor || '#ffffff'};
2572
+ font-size: ${topLineFontSize}px;
2573
+ font-weight: 700;
2574
+ line-height: ${topLineLineHeight};
2575
+ ${topLineMultiLine ? `
2576
+ display: -webkit-box;
2577
+ -webkit-line-clamp: ${topLineMaxLines};
2578
+ -webkit-box-orient: vertical;
2579
+ overflow: hidden;
2580
+ text-overflow: ellipsis;
2581
+ white-space: normal;
2582
+ word-wrap: break-word;
2583
+ ` : `
2584
+ white-space: nowrap;
2585
+ overflow: hidden;
2586
+ text-overflow: ellipsis;
2587
+ `}
2588
+ `;
2589
+ if (firstItem) {
2590
+ if (firstItem.headlineHtml) {
2591
+ headlineText.innerHTML = firstItem.headlineHtml;
2592
+ }
2593
+ else if (firstItem.headline) {
2594
+ headlineText.textContent = firstItem.headline;
2595
+ }
2596
+ else {
2597
+ headlineText.textContent = firstItem.text;
2598
+ }
2599
+ }
2600
+ headlineLine.appendChild(headlineText);
2601
+ body.appendChild(headlineLine);
2602
+ if (twoLineConfig.showSeparator !== false) {
2603
+ const separatorLine = document.createElement('div');
2604
+ separatorLine.style.cssText = `
2605
+ height: 1px;
2606
+ background: ${twoLineConfig.separatorColor || 'rgba(255,255,255,0.2)'};
2607
+ margin: 0 12px;
2608
+ `;
2609
+ body.appendChild(separatorLine);
2610
+ }
2611
+ const detailLine = document.createElement('div');
2612
+ detailLine.className = 'uvf-ticker-detail-line';
2613
+ this.tickerDetailElement = detailLine;
2614
+ const bottomLineHeight = bottomLineConfig.height || 28;
2615
+ const bottomLineFontSize = bottomLineConfig.fontSize || 14;
2616
+ const bottomLineSpeed = bottomLineConfig.speed || 80;
2617
+ detailLine.style.cssText = `
2618
+ display: flex;
2619
+ align-items: center;
2620
+ height: ${bottomLineHeight}px;
2621
+ background: ${bottomLineConfig.backgroundColor || 'transparent'};
2622
+ overflow: hidden;
2623
+ position: relative;
2624
+ `;
2625
+ const track = document.createElement('div');
2626
+ track.className = 'uvf-ticker-track';
2627
+ const detailText = firstItem?.text || '';
2628
+ const textWidth = detailText.length * 10;
2629
+ const duration = Math.max(textWidth / bottomLineSpeed, 10);
2630
+ track.style.cssText = `
2631
+ display: flex;
2632
+ white-space: nowrap;
2633
+ animation: ticker-scroll ${duration}s linear infinite;
2634
+ will-change: transform;
2635
+ padding-left: 100%;
2636
+ `;
2637
+ const renderDetail = (text, html) => {
2638
+ for (let i = 0; i < 10; i++) {
2639
+ const span = document.createElement('span');
2640
+ if (html) {
2641
+ span.innerHTML = html;
2642
+ }
2643
+ else {
2644
+ span.textContent = text;
2645
+ }
2646
+ span.style.cssText = `
2647
+ color: ${bottomLineConfig.textColor || config.textColor || '#ffffff'};
2648
+ font-size: ${bottomLineFontSize}px;
2649
+ font-weight: 500;
2650
+ margin-right: 100px;
2651
+ display: inline-flex;
2652
+ align-items: center;
2653
+ `;
2654
+ track.appendChild(span);
2655
+ }
2656
+ };
2657
+ if (firstItem) {
2658
+ renderDetail(firstItem.text, firstItem.html);
2659
+ }
2660
+ detailLine.appendChild(track);
2661
+ body.appendChild(detailLine);
2662
+ return body;
2663
+ }
2664
+ createProgressBar(itemCycling) {
2665
+ const progressBar = document.createElement('div');
2666
+ progressBar.className = 'uvf-ticker-progress';
2667
+ this.tickerProgressBar = progressBar;
2668
+ const height = itemCycling.progressHeight || 3;
2669
+ progressBar.style.cssText = `
2670
+ height: ${height}px;
2671
+ background: rgba(0,0,0,0.3);
2672
+ position: relative;
2673
+ overflow: hidden;
2674
+ `;
2675
+ const progressFill = document.createElement('div');
2676
+ progressFill.className = 'uvf-progress-fill';
2677
+ this.tickerProgressFill = progressFill;
2678
+ progressFill.style.cssText = `
2679
+ height: 100%;
2680
+ width: 0%;
2681
+ background: ${itemCycling.progressColor || '#ffffff'};
2682
+ transition: width 0.1s linear;
2683
+ `;
2684
+ progressBar.appendChild(progressFill);
2685
+ return progressBar;
2686
+ }
2687
+ createIntroOverlay(broadcastStyle) {
2688
+ const overlay = document.createElement('div');
2689
+ overlay.className = 'uvf-ticker-intro-overlay';
2690
+ overlay.style.cssText = `
2691
+ position: absolute;
2692
+ top: 0;
2693
+ left: 0;
2694
+ right: 0;
2695
+ bottom: 0;
2696
+ display: none;
2697
+ align-items: center;
2698
+ justify-content: center;
2699
+ background: rgba(200, 0, 0, 0.95);
2700
+ z-index: 10;
2701
+ `;
2702
+ const introText = document.createElement('span');
2703
+ introText.className = 'uvf-intro-text';
2704
+ introText.style.cssText = `
2705
+ color: #ffffff;
2706
+ font-size: 24px;
2707
+ font-weight: 900;
2708
+ text-transform: uppercase;
2709
+ letter-spacing: 3px;
2710
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
2711
+ `;
2712
+ introText.textContent = 'BREAKING';
2713
+ overlay.appendChild(introText);
2714
+ return overlay;
2715
+ }
2716
+ playIntroAnimation(item) {
2717
+ return new Promise((resolve) => {
2718
+ if (!this.tickerIntroOverlay || !item.showIntro) {
2719
+ resolve();
2720
+ return;
2721
+ }
2722
+ const broadcastStyle = this.tickerConfig?.broadcastStyle || {};
2723
+ const animation = item.introAnimation || broadcastStyle.defaultIntroAnimation || 'slide-in';
2724
+ const duration = item.introDuration || broadcastStyle.defaultIntroDuration || 2000;
2725
+ const introText = item.introText || 'BREAKING';
2726
+ const textElement = this.tickerIntroOverlay.querySelector('.uvf-intro-text');
2727
+ if (textElement) {
2728
+ textElement.textContent = introText;
2729
+ }
2730
+ this.tickerIntroOverlay.style.display = 'flex';
2731
+ this.tickerIntroOverlay.style.animation = `intro-${animation} ${duration}ms ease-out forwards`;
2732
+ setTimeout(() => {
2733
+ if (this.tickerIntroOverlay) {
2734
+ this.tickerIntroOverlay.style.display = 'none';
2735
+ this.tickerIntroOverlay.style.animation = '';
2736
+ }
2737
+ resolve();
2738
+ }, duration);
2739
+ });
2740
+ }
2741
+ startItemCycling() {
2742
+ if (!this.tickerConfig?.items || this.tickerConfig.items.length <= 1)
2743
+ return;
2744
+ const itemCycling = this.tickerConfig.itemCycling || {};
2745
+ if (!itemCycling.enabled)
2746
+ return;
2747
+ const currentItem = this.tickerConfig.items[this.tickerCurrentItemIndex];
2748
+ const duration = (currentItem.duration || itemCycling.defaultDuration || 10) * 1000;
2749
+ this.animateProgress(duration);
2750
+ this.tickerCycleTimer = window.setTimeout(() => {
2751
+ this.transitionToNextItem();
2752
+ }, duration);
2753
+ }
2754
+ stopItemCycling() {
2755
+ if (this.tickerCycleTimer) {
2756
+ clearTimeout(this.tickerCycleTimer);
2757
+ this.tickerCycleTimer = null;
2758
+ }
2759
+ }
2760
+ pauseItemCycling() {
2761
+ if (!this.tickerCycleTimer || this.tickerIsPaused)
2762
+ return;
2763
+ this.tickerIsPaused = true;
2764
+ this.tickerPauseStartTime = Date.now();
2765
+ const itemCycling = this.tickerConfig?.itemCycling || {};
2766
+ const currentItem = this.tickerConfig?.items?.[this.tickerCurrentItemIndex];
2767
+ const totalDuration = (currentItem?.duration || itemCycling.defaultDuration || 10) * 1000;
2768
+ clearTimeout(this.tickerCycleTimer);
2769
+ this.tickerCycleTimer = null;
2770
+ if (this.tickerProgressFill) {
2771
+ const computedWidth = window.getComputedStyle(this.tickerProgressFill).width;
2772
+ this.tickerProgressFill.style.transition = 'none';
2773
+ this.tickerProgressFill.style.width = computedWidth;
2774
+ }
2775
+ }
2776
+ resumeItemCycling() {
2777
+ if (!this.tickerIsPaused)
2778
+ return;
2779
+ this.tickerIsPaused = false;
2780
+ const itemCycling = this.tickerConfig?.itemCycling || {};
2781
+ const currentItem = this.tickerConfig?.items?.[this.tickerCurrentItemIndex];
2782
+ const totalDuration = (currentItem?.duration || itemCycling.defaultDuration || 10) * 1000;
2783
+ let remainingTime = totalDuration;
2784
+ if (this.tickerProgressFill) {
2785
+ const currentWidth = parseFloat(this.tickerProgressFill.style.width) || 0;
2786
+ remainingTime = totalDuration * (1 - currentWidth / 100);
2787
+ }
2788
+ this.animateProgress(remainingTime);
2789
+ this.tickerCycleTimer = window.setTimeout(() => {
2790
+ this.transitionToNextItem();
2791
+ }, remainingTime);
2792
+ }
2793
+ animateProgress(duration) {
2794
+ if (!this.tickerProgressFill)
2795
+ return;
2796
+ this.tickerProgressFill.style.transition = 'none';
2797
+ this.tickerProgressFill.style.width = '0%';
2798
+ this.tickerProgressFill.offsetHeight;
2799
+ this.tickerProgressFill.style.transition = `width ${duration}ms linear`;
2800
+ this.tickerProgressFill.style.width = '100%';
2801
+ }
2802
+ async transitionToNextItem() {
2803
+ if (!this.tickerConfig?.items || this.tickerConfig.items.length <= 1)
2804
+ return;
2805
+ const itemCycling = this.tickerConfig.itemCycling || {};
2806
+ const transitionType = itemCycling.transitionType || 'fade';
2807
+ const transitionDuration = itemCycling.transitionDuration || 500;
2808
+ this.tickerCurrentItemIndex = (this.tickerCurrentItemIndex + 1) % this.tickerConfig.items.length;
2809
+ const nextItem = this.tickerConfig.items[this.tickerCurrentItemIndex];
2810
+ if (nextItem.showIntro) {
2811
+ await this.playIntroAnimation(nextItem);
2812
+ }
2813
+ await this.applyItemTransition(transitionType, transitionDuration, nextItem);
2814
+ this.startItemCycling();
2815
+ }
2816
+ applyItemTransition(transitionType, duration, item) {
2817
+ return new Promise((resolve) => {
2818
+ const headline = this.tickerHeadlineElement?.querySelector('.uvf-ticker-headline-text');
2819
+ const detailTrack = this.tickerDetailElement?.querySelector('.uvf-ticker-track');
2820
+ if (!headline || !detailTrack) {
2821
+ this.updateItemContent(item);
2822
+ resolve();
2823
+ return;
2824
+ }
2825
+ if (transitionType === 'none') {
2826
+ this.updateItemContent(item);
2827
+ resolve();
2828
+ return;
2829
+ }
2830
+ const outAnimation = transitionType === 'slide' ? 'item-slide-out' : 'item-fade-out';
2831
+ const inAnimation = transitionType === 'slide' ? 'item-slide-in' : 'item-fade-in';
2832
+ headline.style.animation = `${outAnimation} ${duration / 2}ms ease-out forwards`;
2833
+ setTimeout(() => {
2834
+ this.updateItemContent(item);
2835
+ headline.style.animation = `${inAnimation} ${duration / 2}ms ease-out forwards`;
2836
+ setTimeout(() => {
2837
+ headline.style.animation = '';
2838
+ resolve();
2839
+ }, duration / 2);
2840
+ }, duration / 2);
2841
+ });
2842
+ }
2843
+ updateItemContent(item) {
2844
+ const headline = this.tickerHeadlineElement?.querySelector('.uvf-ticker-headline-text');
2845
+ if (headline) {
2846
+ if (item.headlineHtml) {
2847
+ headline.innerHTML = item.headlineHtml;
2848
+ }
2849
+ else if (item.headline) {
2850
+ headline.textContent = item.headline;
2851
+ }
2852
+ else {
2853
+ headline.textContent = item.text;
2854
+ }
2855
+ }
2856
+ const detailTrack = this.tickerDetailElement?.querySelector('.uvf-ticker-track');
2857
+ if (detailTrack) {
2858
+ detailTrack.innerHTML = '';
2859
+ const bottomLineConfig = this.tickerConfig?.broadcastStyle?.twoLineDisplay?.bottomLine || {};
2860
+ const bottomLineFontSize = bottomLineConfig.fontSize || 14;
2861
+ for (let i = 0; i < 10; i++) {
2862
+ const span = document.createElement('span');
2863
+ if (item.html) {
2864
+ span.innerHTML = item.html;
2865
+ }
2866
+ else {
2867
+ span.textContent = item.text;
2868
+ }
2869
+ span.style.cssText = `
2870
+ color: ${bottomLineConfig.textColor || this.tickerConfig?.textColor || '#ffffff'};
2871
+ font-size: ${bottomLineFontSize}px;
2872
+ font-weight: 500;
2873
+ margin-right: 100px;
2874
+ display: inline-flex;
2875
+ align-items: center;
2876
+ `;
2877
+ detailTrack.appendChild(span);
2878
+ }
2879
+ const bottomLineSpeed = bottomLineConfig.speed || 80;
2880
+ const textWidth = item.text.length * 10;
2881
+ const duration = Math.max(textWidth / bottomLineSpeed, 10);
2882
+ detailTrack.style.animation = `ticker-scroll ${duration}s linear infinite`;
2883
+ }
2884
+ }
2214
2885
  setAutoQuality(enabled) {
2215
2886
  this.autoQuality = enabled;
2216
2887
  if (this.hls) {