unified-video-framework 1.4.436 → 1.4.438

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.
@@ -5,9 +5,33 @@ import { DRMType as DRMTypeEnum } from "../../../../core/dist/index.js";
5
5
  export class WidevineDRM extends BaseDRM {
6
6
  async initialize() {
7
7
  this.log('Initializing Widevine DRM...');
8
+ this.log('Requested robustness - Video:', this.config.videoRobustness || 'SW_SECURE_CRYPTO', 'Audio:', this.config.audioRobustness || 'SW_SECURE_CRYPTO');
8
9
  try {
9
10
  const keySystemAccess = await navigator.requestMediaKeySystemAccess(this.getKeySystem(), this.getKeySystemConfiguration());
10
11
  this.log('Widevine key system access granted');
12
+
13
+ // Log the actual configuration that was accepted
14
+ const actualConfig = keySystemAccess.getConfiguration();
15
+ this.log('✅ GRANTED Configuration:', {
16
+ videoRobustness: actualConfig.videoCapabilities?.[0]?.robustness || 'unknown',
17
+ audioRobustness: actualConfig.audioCapabilities?.[0]?.robustness || 'unknown',
18
+ distinctiveIdentifier: actualConfig.distinctiveIdentifier,
19
+ persistentState: actualConfig.persistentState
20
+ });
21
+
22
+ // Check if fallback happened
23
+ const requestedVideoRobustness = this.config.videoRobustness || 'SW_SECURE_CRYPTO';
24
+ const grantedVideoRobustness = actualConfig.videoCapabilities?.[0]?.robustness;
25
+
26
+ if (requestedVideoRobustness !== grantedVideoRobustness) {
27
+ this.log('⚠️ WARNING: Browser fell back from', requestedVideoRobustness, 'to', grantedVideoRobustness);
28
+ this.log('⚠️ Screenshot blocking may not work - system does not support hardware overlay');
29
+ } else if (requestedVideoRobustness === 'HW_SECURE_DECODE' && grantedVideoRobustness === 'HW_SECURE_DECODE') {
30
+ this.log('✅ SUCCESS: Hardware overlay protection active - screenshots should show black screen');
31
+ } else {
32
+ this.log('ℹ️ Using software DRM - screenshots will be visible');
33
+ }
34
+
11
35
  this.mediaKeys = await keySystemAccess.createMediaKeys();
12
36
  }
13
37
  catch (error) {
@@ -22,12 +46,15 @@ export class WidevineDRM extends BaseDRM {
22
46
  getHLSConfig() {
23
47
  this.log('Generating HLS.js config for Widevine');
24
48
  const licenseUrl = this.config.licenseUrl || this.config.widevineOptions?.licenseUrl;
49
+ // Use videoRobustness from config if provided, otherwise fallback to SW_SECURE_CRYPTO
50
+ const robustness = this.config.videoRobustness || 'SW_SECURE_CRYPTO';
51
+ this.log(`Using video robustness level: ${robustness}`);
25
52
  return {
26
53
  emeEnabled: true,
27
54
  drmSystems: {
28
55
  [this.getKeySystem()]: {
29
56
  licenseUrl: licenseUrl,
30
- robustness: 'SW_SECURE_CRYPTO',
57
+ robustness: robustness,
31
58
  },
32
59
  },
33
60
  requestMediaKeySystemAccessFunc: navigator.requestMediaKeySystemAccess.bind(navigator),
@@ -70,12 +97,46 @@ export class WidevineDRM extends BaseDRM {
70
97
  return protectionData;
71
98
  }
72
99
  getKeySystemConfiguration() {
73
- return [{
100
+ // Use robustness from config or fallback to SW_SECURE_CRYPTO (L3)
101
+ const videoRobustness = this.config.videoRobustness || 'SW_SECURE_CRYPTO';
102
+ const audioRobustness = this.config.audioRobustness || 'SW_SECURE_CRYPTO';
103
+
104
+ this.log(`Key system config - Video: ${videoRobustness}, Audio: ${audioRobustness}`);
105
+
106
+ // Create configuration with multiple robustness fallbacks
107
+ const configs = [];
108
+
109
+ // Try requested robustness first
110
+ configs.push({
111
+ initDataTypes: ['cenc'],
112
+ audioCapabilities: [{
113
+ contentType: 'audio/mp4; codecs="mp4a.40.2"',
114
+ robustness: audioRobustness,
115
+ }],
116
+ videoCapabilities: [
117
+ {
118
+ contentType: 'video/mp4; codecs="avc1.42E01E"',
119
+ robustness: videoRobustness,
120
+ },
121
+ {
122
+ contentType: 'video/mp4; codecs="avc1.4d401f"',
123
+ robustness: videoRobustness,
124
+ },
125
+ ],
126
+ distinctiveIdentifier: 'optional',
127
+ persistentState: 'optional',
128
+ sessionTypes: ['temporary'],
129
+ });
130
+
131
+ // Fallback to SW_SECURE_CRYPTO if hardware not available
132
+ if (videoRobustness !== 'SW_SECURE_CRYPTO') {
133
+ this.log('Adding fallback configuration with SW_SECURE_CRYPTO');
134
+ configs.push({
74
135
  initDataTypes: ['cenc'],
75
136
  audioCapabilities: [{
76
- contentType: 'audio/mp4; codecs="mp4a.40.2"',
77
- robustness: 'SW_SECURE_CRYPTO',
78
- }],
137
+ contentType: 'audio/mp4; codecs="mp4a.40.2"',
138
+ robustness: 'SW_SECURE_CRYPTO',
139
+ }],
79
140
  videoCapabilities: [
80
141
  {
81
142
  contentType: 'video/mp4; codecs="avc1.42E01E"',
@@ -89,7 +150,10 @@ export class WidevineDRM extends BaseDRM {
89
150
  distinctiveIdentifier: 'not-allowed',
90
151
  persistentState: 'not-allowed',
91
152
  sessionTypes: ['temporary'],
92
- }];
153
+ });
154
+ }
155
+
156
+ return configs;
93
157
  }
94
158
  }
95
159
  //# sourceMappingURL=WidevineDRM.js.map
@@ -2,5 +2,6 @@ export * from '../../core/dist/index';
2
2
  export { WebPlayer } from './WebPlayer';
3
3
  export { WebPlayerView } from './react/WebPlayerView';
4
4
  export * from './react/EPG';
5
+ export type { FlashNewsTickerConfig, FlashNewsTickerItem, FlashNewsTickerAPI, TickerDisplayConfig, TickerStyleVariant, BroadcastTheme, BroadcastStyleConfig } from './react/types/FlashNewsTickerTypes';
5
6
  export declare const VERSION = "1.0.0";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,uBAAuB,CAAC;AAGtC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGtD,cAAc,aAAa,CAAC;AAG5B,eAAO,MAAM,OAAO,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,uBAAuB,CAAC;AAGtC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGtD,cAAc,aAAa,CAAC;AAG5B,YAAY,EACV,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACrB,MAAM,oCAAoC,CAAC;AAG5C,eAAO,MAAM,OAAO,UAAU,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,uBAAuB,CAAC;AAGtC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGtD,cAAc,aAAa,CAAC;AAG5B,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,uBAAuB,CAAC;AAGtC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGtD,cAAc,aAAa,CAAC;AAc5B,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
@@ -1,3 +1,19 @@
1
+ export type TickerStyleVariant = 'simple' | 'broadcast';
2
+ export type BroadcastTheme = 'breaking-red' | 'breaking-blue' | 'alert-red' | 'news-blue' | 'custom';
3
+ export interface BroadcastStyleConfig {
4
+ theme?: BroadcastTheme;
5
+ headerText?: string;
6
+ showGlobe?: boolean;
7
+ showLiveBadge?: boolean;
8
+ headerColor?: string;
9
+ headerTextColor?: string;
10
+ bodyColor?: string;
11
+ headerHeight?: number;
12
+ headerFontSize?: number;
13
+ subHeaderText?: string;
14
+ animateGlobe?: boolean;
15
+ pulseLiveBadge?: boolean;
16
+ }
1
17
  export interface FlashNewsTickerItem {
2
18
  id: string;
3
19
  text: string;
@@ -18,11 +34,15 @@ export interface TickerDisplayConfig {
18
34
  gap?: number;
19
35
  separator?: string;
20
36
  offset?: number;
37
+ styleVariant?: TickerStyleVariant;
38
+ broadcastStyle?: BroadcastStyleConfig;
21
39
  }
22
40
  export interface FlashNewsTickerConfig {
23
41
  enabled?: boolean;
24
42
  items?: FlashNewsTickerItem[];
25
43
  position?: 'top' | 'bottom' | 'both';
44
+ styleVariant?: TickerStyleVariant;
45
+ broadcastStyle?: BroadcastStyleConfig;
26
46
  topConfig?: TickerDisplayConfig;
27
47
  bottomConfig?: TickerDisplayConfig;
28
48
  height?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"FlashNewsTickerTypes.d.ts","sourceRoot":"","sources":["../../../src/react/types/FlashNewsTickerTypes.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,mBAAmB;IAElC,EAAE,EAAE,MAAM,CAAC;IAGX,IAAI,EAAE,MAAM,CAAC;IAOb,IAAI,CAAC,EAAE,MAAM,CAAC;IAGd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAChC;AAKD,MAAM,WAAW,mBAAmB;IAElC,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAK7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAGhB,eAAe,CAAC,EAAE,MAAM,CAAC;IAGzB,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAK7B,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,GAAG,CAAC,EAAE,MAAM,CAAC;IAGb,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAKD,MAAM,WAAW,qBAAqB;IAEpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAGlB,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAG9B,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAGrC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAGhC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAKnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAGhB,eAAe,CAAC,EAAE,MAAM,CAAC;IAGzB,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAK7B,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,YAAY,CAAC,EAAE,OAAO,CAAC;IAGvB,GAAG,CAAC,EAAE,MAAM,CAAC;IAGb,IAAI,CAAC,EAAE,OAAO,CAAC;IAGf,SAAS,CAAC,EAAE,MAAM,CAAC;IAKnB,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAKD,MAAM,WAAW,kBAAkB;IAEjC,IAAI,EAAE,MAAM,IAAI,CAAC;IAGjB,IAAI,EAAE,MAAM,IAAI,CAAC;IAGjB,SAAS,EAAE,MAAM,OAAO,CAAC;IAGzB,WAAW,EAAE,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,IAAI,CAAC;IAGpD,OAAO,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAG7C,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAGrC,UAAU,EAAE,MAAM,IAAI,CAAC;IAGvB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,qBAAqB,CAAC,KAAK,IAAI,CAAC;IAG/D,KAAK,EAAE,MAAM,IAAI,CAAC;IAGlB,MAAM,EAAE,MAAM,IAAI,CAAC;IAGnB,QAAQ,EAAE,MAAM,OAAO,CAAC;CACzB"}
1
+ {"version":3,"file":"FlashNewsTickerTypes.d.ts","sourceRoot":"","sources":["../../../src/react/types/FlashNewsTickerTypes.ts"],"names":[],"mappings":"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"}
@@ -20,7 +20,7 @@ import {
20
20
  VideoSegment,
21
21
  ChapterEvents
22
22
  } from './chapters/types/ChapterTypes';
23
- import type { FlashNewsTickerConfig } from './react/types/FlashNewsTickerTypes';
23
+ import type { FlashNewsTickerConfig, BroadcastStyleConfig, BroadcastTheme, TickerStyleVariant } from './react/types/FlashNewsTickerTypes';
24
24
  import YouTubeExtractor from './utils/YouTubeExtractor';
25
25
  import { DRMManager, DRMErrorHandler } from './drm';
26
26
  import type { DRMInitResult, DRMError } from './drm';
@@ -172,6 +172,8 @@ export class WebPlayer extends BasePlayer {
172
172
  private currentRetryAttempt: number = 0;
173
173
  private lastFailedUrl: string = ''; // Track last failed URL to avoid duplicate error handling
174
174
  private isFallbackPosterMode: boolean = false; // True when showing fallback poster (no playable sources)
175
+ private hlsErrorRetryCount: number = 0; // Track HLS error recovery attempts
176
+ private readonly MAX_HLS_ERROR_RETRIES: number = 3; // Max HLS recovery attempts before fallback
175
177
 
176
178
  // Live stream detection
177
179
  private lastDuration: number = 0; // Track duration changes to detect live streams
@@ -727,6 +729,7 @@ export class WebPlayer extends BasePlayer {
727
729
  this.currentRetryAttempt = 0;
728
730
  this.lastFailedUrl = '';
729
731
  this.isFallbackPosterMode = false; // Reset fallback poster mode
732
+ this.hlsErrorRetryCount = 0; // Reset HLS error retry count
730
733
 
731
734
  // Clean up previous instances
732
735
  await this.cleanup();
@@ -1122,6 +1125,9 @@ export class WebPlayer extends BasePlayer {
1122
1125
  this.hls.attachMedia(this.video);
1123
1126
 
1124
1127
  this.hls.on(window.Hls.Events.MANIFEST_PARSED, (event: any, data: any) => {
1128
+ // Reset HLS error retry count on successful manifest load
1129
+ this.hlsErrorRetryCount = 0;
1130
+
1125
1131
  // Extract quality levels
1126
1132
  this.qualities = data.levels.map((level: any, index: number) => ({
1127
1133
  height: level.height,
@@ -1164,27 +1170,101 @@ export class WebPlayer extends BasePlayer {
1164
1170
  }
1165
1171
  }
1166
1172
 
1167
- private handleHLSError(data: any): void {
1173
+ private async handleHLSError(data: any): Promise<void> {
1168
1174
  const Hls = window.Hls;
1175
+
1176
+ this.debugLog(`🔴 HLS Error: type=${data.type}, details=${data.details}, fatal=${data.fatal}`);
1177
+
1178
+ // Check if we've exceeded max retry attempts
1179
+ if (this.hlsErrorRetryCount >= this.MAX_HLS_ERROR_RETRIES) {
1180
+ this.debugLog(`🔴 HLS max retries (${this.MAX_HLS_ERROR_RETRIES}) exceeded, triggering fallback`);
1181
+ this.hls?.destroy();
1182
+ this.hls = null;
1183
+
1184
+ // Try fallback sources
1185
+ const fallbackLoaded = await this.tryFallbackSource({
1186
+ code: 'HLS_ERROR',
1187
+ message: data.details,
1188
+ type: data.type,
1189
+ fatal: true,
1190
+ details: data
1191
+ });
1192
+
1193
+ if (!fallbackLoaded) {
1194
+ this.handleError({
1195
+ code: 'HLS_ERROR',
1196
+ message: `HLS stream failed after ${this.MAX_HLS_ERROR_RETRIES} retries: ${data.details}`,
1197
+ type: 'media',
1198
+ fatal: true,
1199
+ details: data
1200
+ });
1201
+ }
1202
+ return;
1203
+ }
1204
+
1169
1205
  switch (data.type) {
1170
1206
  case Hls.ErrorTypes.NETWORK_ERROR:
1171
- console.error('Fatal network error, trying to recover');
1172
- this.hls.startLoad();
1207
+ this.hlsErrorRetryCount++;
1208
+ this.debugLog(`🔴 Fatal network error (attempt ${this.hlsErrorRetryCount}/${this.MAX_HLS_ERROR_RETRIES}), trying to recover`);
1209
+
1210
+ // For manifest errors (like 404), recovery won't help - go straight to fallback
1211
+ if (data.details === 'manifestLoadError' || data.details === 'manifestParsingError') {
1212
+ this.debugLog(`🔴 Manifest error detected (${data.details}), skipping recovery - triggering fallback`);
1213
+ this.hls?.destroy();
1214
+ this.hls = null;
1215
+
1216
+ const fallbackLoaded = await this.tryFallbackSource({
1217
+ code: 'HLS_MANIFEST_ERROR',
1218
+ message: data.details,
1219
+ type: 'network',
1220
+ fatal: true,
1221
+ details: data
1222
+ });
1223
+
1224
+ if (!fallbackLoaded) {
1225
+ this.handleError({
1226
+ code: 'HLS_MANIFEST_ERROR',
1227
+ message: `Failed to load HLS manifest: ${data.details}`,
1228
+ type: 'media',
1229
+ fatal: true,
1230
+ details: data
1231
+ });
1232
+ }
1233
+ } else {
1234
+ // For other network errors, try recovery
1235
+ this.hls?.startLoad();
1236
+ }
1173
1237
  break;
1238
+
1174
1239
  case Hls.ErrorTypes.MEDIA_ERROR:
1175
- console.error('Fatal media error, trying to recover');
1176
- this.hls.recoverMediaError();
1240
+ this.hlsErrorRetryCount++;
1241
+ this.debugLog(`🔴 Fatal media error (attempt ${this.hlsErrorRetryCount}/${this.MAX_HLS_ERROR_RETRIES}), trying to recover`);
1242
+ this.hls?.recoverMediaError();
1177
1243
  break;
1244
+
1178
1245
  default:
1179
- console.error('Fatal error, cannot recover');
1180
- this.handleError({
1246
+ this.debugLog(`🔴 Fatal unrecoverable HLS error: ${data.details}`);
1247
+ this.hls?.destroy();
1248
+ this.hls = null;
1249
+
1250
+ // Try fallback sources for unrecoverable errors
1251
+ const fallbackLoaded = await this.tryFallbackSource({
1181
1252
  code: 'HLS_ERROR',
1182
1253
  message: data.details,
1183
1254
  type: 'media',
1184
1255
  fatal: true,
1185
1256
  details: data
1186
1257
  });
1187
- this.hls.destroy();
1258
+
1259
+ if (!fallbackLoaded) {
1260
+ this.handleError({
1261
+ code: 'HLS_ERROR',
1262
+ message: data.details,
1263
+ type: 'media',
1264
+ fatal: true,
1265
+ details: data
1266
+ });
1267
+ }
1188
1268
  break;
1189
1269
  }
1190
1270
  }
@@ -2468,8 +2548,18 @@ export class WebPlayer extends BasePlayer {
2468
2548
  }
2469
2549
 
2470
2550
  private createTickerElement(config: FlashNewsTickerConfig, position: 'top' | 'bottom'): HTMLDivElement {
2551
+ // Route to broadcast style if configured
2552
+ if (config.styleVariant === 'broadcast') {
2553
+ return this.createBroadcastTickerElement(config, position);
2554
+ }
2555
+
2556
+ // Simple style (original implementation)
2557
+ return this.createSimpleTickerElement(config, position);
2558
+ }
2559
+
2560
+ private createSimpleTickerElement(config: FlashNewsTickerConfig, position: 'top' | 'bottom'): HTMLDivElement {
2471
2561
  const ticker = document.createElement('div');
2472
- ticker.className = `uvf-flash-ticker ticker-${position}`;
2562
+ ticker.className = `uvf-flash-ticker ticker-${position} ticker-simple`;
2473
2563
 
2474
2564
  const bottomOffset = config.bottomOffset || 0;
2475
2565
  const topOffset = config.topOffset || 0;
@@ -2567,6 +2657,262 @@ export class WebPlayer extends BasePlayer {
2567
2657
  ticker.appendChild(track);
2568
2658
 
2569
2659
  // Add animation keyframes to document if not exists
2660
+ this.ensureTickerAnimations();
2661
+
2662
+ return ticker;
2663
+ }
2664
+
2665
+ private createBroadcastTickerElement(config: FlashNewsTickerConfig, position: 'top' | 'bottom'): HTMLDivElement {
2666
+ const broadcastStyle = config.broadcastStyle || {};
2667
+ const theme = broadcastStyle.theme || 'breaking-red';
2668
+
2669
+ // Get theme colors
2670
+ const themeColors = this.getBroadcastThemeColors(theme, broadcastStyle);
2671
+
2672
+ const ticker = document.createElement('div');
2673
+ ticker.className = `uvf-flash-ticker ticker-${position} ticker-broadcast`;
2674
+
2675
+ const bottomOffset = config.bottomOffset || 0;
2676
+ const topOffset = config.topOffset || 0;
2677
+ const headerHeight = broadcastStyle.headerHeight || 28;
2678
+ const bodyHeight = config.height || 36;
2679
+ const totalHeight = headerHeight + bodyHeight;
2680
+
2681
+ ticker.style.cssText = `
2682
+ position: absolute;
2683
+ left: 0;
2684
+ right: 0;
2685
+ height: ${totalHeight}px;
2686
+ ${position === 'top' ? `top: ${topOffset}px;` : `bottom: ${bottomOffset}px;`}
2687
+ overflow: hidden;
2688
+ pointer-events: none;
2689
+ display: flex;
2690
+ flex-direction: column;
2691
+ `;
2692
+
2693
+ // Create header row (BREAKING NEWS with globe and LIVE badge)
2694
+ const header = document.createElement('div');
2695
+ header.className = 'uvf-ticker-header';
2696
+ header.style.cssText = `
2697
+ display: flex;
2698
+ align-items: center;
2699
+ height: ${headerHeight}px;
2700
+ background: ${themeColors.headerBg};
2701
+ padding: 0 12px;
2702
+ position: relative;
2703
+ `;
2704
+
2705
+ // Add globe graphic if enabled
2706
+ if (broadcastStyle.showGlobe !== false) {
2707
+ const globe = this.createGlobeElement(broadcastStyle.animateGlobe !== false);
2708
+ header.appendChild(globe);
2709
+ }
2710
+
2711
+ // Add header text (BREAKING NEWS)
2712
+ const headerText = document.createElement('span');
2713
+ headerText.className = 'uvf-ticker-header-text';
2714
+ headerText.textContent = broadcastStyle.headerText || 'BREAKING NEWS';
2715
+ headerText.style.cssText = `
2716
+ color: ${broadcastStyle.headerTextColor || '#ffffff'};
2717
+ font-size: ${broadcastStyle.headerFontSize || 16}px;
2718
+ font-weight: 800;
2719
+ text-transform: uppercase;
2720
+ letter-spacing: 1px;
2721
+ margin-left: ${broadcastStyle.showGlobe !== false ? '8px' : '0'};
2722
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
2723
+ `;
2724
+ header.appendChild(headerText);
2725
+
2726
+ // Add LIVE badge if enabled
2727
+ if (broadcastStyle.showLiveBadge !== false) {
2728
+ const liveBadge = this.createLiveBadgeElement(broadcastStyle.pulseLiveBadge !== false);
2729
+ header.appendChild(liveBadge);
2730
+ }
2731
+
2732
+ // Create body row (scrolling ticker)
2733
+ const body = document.createElement('div');
2734
+ body.className = 'uvf-ticker-body';
2735
+ body.style.cssText = `
2736
+ display: flex;
2737
+ align-items: center;
2738
+ height: ${bodyHeight}px;
2739
+ background: ${themeColors.bodyBg};
2740
+ overflow: hidden;
2741
+ position: relative;
2742
+ `;
2743
+
2744
+ // Create scrolling track
2745
+ const track = document.createElement('div');
2746
+ track.className = 'uvf-ticker-track';
2747
+
2748
+ const duration = this.calculateTickerDuration(config);
2749
+ track.style.cssText = `
2750
+ display: flex;
2751
+ white-space: nowrap;
2752
+ animation: ticker-scroll ${duration}s linear infinite;
2753
+ will-change: transform;
2754
+ padding-left: 100%;
2755
+ `;
2756
+
2757
+ // Calculate responsive font size
2758
+ const containerWidth = this.container?.offsetWidth || 1920;
2759
+ const baseFontSize = config.fontSize || 14;
2760
+ let responsiveFontSize = baseFontSize;
2761
+ if (containerWidth < 768) {
2762
+ responsiveFontSize = Math.max(baseFontSize * 0.8, 12);
2763
+ } else if (containerWidth < 1280) {
2764
+ responsiveFontSize = Math.max(baseFontSize * 0.9, 13);
2765
+ }
2766
+
2767
+ // Render items
2768
+ const renderItems = () => {
2769
+ if (!config.items) return;
2770
+ config.items.forEach((item) => {
2771
+ const span = document.createElement('span');
2772
+ if (item.html) {
2773
+ span.innerHTML = item.html;
2774
+ } else {
2775
+ span.textContent = item.text;
2776
+ }
2777
+ span.style.cssText = `
2778
+ color: ${config.textColor || '#ffffff'};
2779
+ font-size: ${responsiveFontSize}px;
2780
+ font-weight: ${config.fontWeight || 600};
2781
+ margin-right: ${config.gap || 100}px;
2782
+ display: inline-flex;
2783
+ align-items: center;
2784
+ `;
2785
+ track.appendChild(span);
2786
+
2787
+ if (config.separator) {
2788
+ const sep = document.createElement('span');
2789
+ sep.textContent = config.separator;
2790
+ sep.style.cssText = `
2791
+ color: ${config.textColor || '#ffffff'};
2792
+ font-size: ${responsiveFontSize}px;
2793
+ opacity: 0.5;
2794
+ margin: 0 8px;
2795
+ `;
2796
+ track.appendChild(sep);
2797
+ }
2798
+ });
2799
+ };
2800
+
2801
+ for (let i = 0; i < 10; i++) {
2802
+ renderItems();
2803
+ }
2804
+
2805
+ body.appendChild(track);
2806
+ ticker.appendChild(header);
2807
+ ticker.appendChild(body);
2808
+
2809
+ // Add all animation keyframes
2810
+ this.ensureTickerAnimations();
2811
+
2812
+ return ticker;
2813
+ }
2814
+
2815
+ private getBroadcastThemeColors(theme: BroadcastTheme, broadcastStyle: BroadcastStyleConfig): { headerBg: string; bodyBg: string } {
2816
+ switch (theme) {
2817
+ case 'breaking-red':
2818
+ return {
2819
+ headerBg: 'linear-gradient(90deg, #cc0000 0%, #ff0000 50%, #cc0000 100%)',
2820
+ bodyBg: 'linear-gradient(90deg, #1a237e 0%, #283593 50%, #1a237e 100%)'
2821
+ };
2822
+ case 'breaking-blue':
2823
+ return {
2824
+ headerBg: 'linear-gradient(90deg, #0d47a1 0%, #1565c0 50%, #0d47a1 100%)',
2825
+ bodyBg: 'linear-gradient(90deg, #1a1a2e 0%, #16213e 50%, #1a1a2e 100%)'
2826
+ };
2827
+ case 'alert-red':
2828
+ return {
2829
+ headerBg: 'linear-gradient(90deg, #b71c1c 0%, #d32f2f 50%, #b71c1c 100%)',
2830
+ bodyBg: 'linear-gradient(90deg, #7f0000 0%, #9a0000 50%, #7f0000 100%)'
2831
+ };
2832
+ case 'news-blue':
2833
+ return {
2834
+ headerBg: 'linear-gradient(90deg, #01579b 0%, #0277bd 50%, #01579b 100%)',
2835
+ bodyBg: 'linear-gradient(90deg, #002171 0%, #003c8f 50%, #002171 100%)'
2836
+ };
2837
+ case 'custom':
2838
+ return {
2839
+ headerBg: broadcastStyle.headerColor || '#cc0000',
2840
+ bodyBg: broadcastStyle.bodyColor || '#1a237e'
2841
+ };
2842
+ default:
2843
+ return {
2844
+ headerBg: 'linear-gradient(90deg, #cc0000 0%, #ff0000 50%, #cc0000 100%)',
2845
+ bodyBg: 'linear-gradient(90deg, #1a237e 0%, #283593 50%, #1a237e 100%)'
2846
+ };
2847
+ }
2848
+ }
2849
+
2850
+ private createGlobeElement(animate: boolean): HTMLDivElement {
2851
+ const globeContainer = document.createElement('div');
2852
+ globeContainer.className = 'uvf-ticker-globe';
2853
+ globeContainer.style.cssText = `
2854
+ width: 24px;
2855
+ height: 24px;
2856
+ position: relative;
2857
+ flex-shrink: 0;
2858
+ `;
2859
+
2860
+ // SVG Globe icon
2861
+ globeContainer.innerHTML = `
2862
+ <svg viewBox="0 0 24 24" fill="none" style="width: 100%; height: 100%; ${animate ? 'animation: globe-rotate 8s linear infinite;' : ''}">
2863
+ <circle cx="12" cy="12" r="10" stroke="#ffffff" stroke-width="1.5" fill="none"/>
2864
+ <ellipse cx="12" cy="12" rx="10" ry="4" stroke="#ffffff" stroke-width="1" fill="none"/>
2865
+ <ellipse cx="12" cy="12" rx="4" ry="10" stroke="#ffffff" stroke-width="1" fill="none"/>
2866
+ <line x1="2" y1="12" x2="22" y2="12" stroke="#ffffff" stroke-width="0.5"/>
2867
+ <line x1="12" y1="2" x2="12" y2="22" stroke="#ffffff" stroke-width="0.5"/>
2868
+ <path d="M4 8 Q12 6 20 8" stroke="#ffffff" stroke-width="0.5" fill="none"/>
2869
+ <path d="M4 16 Q12 18 20 16" stroke="#ffffff" stroke-width="0.5" fill="none"/>
2870
+ </svg>
2871
+ `;
2872
+
2873
+ return globeContainer;
2874
+ }
2875
+
2876
+ private createLiveBadgeElement(pulse: boolean): HTMLDivElement {
2877
+ const badge = document.createElement('div');
2878
+ badge.className = 'uvf-ticker-live-badge';
2879
+ badge.style.cssText = `
2880
+ display: flex;
2881
+ align-items: center;
2882
+ margin-left: auto;
2883
+ padding: 2px 8px;
2884
+ background: #ff0000;
2885
+ border-radius: 3px;
2886
+ ${pulse ? 'animation: live-pulse 1.5s ease-in-out infinite;' : ''}
2887
+ `;
2888
+
2889
+ // Red dot
2890
+ const dot = document.createElement('span');
2891
+ dot.style.cssText = `
2892
+ width: 8px;
2893
+ height: 8px;
2894
+ background: #ffffff;
2895
+ border-radius: 50%;
2896
+ margin-right: 4px;
2897
+ ${pulse ? 'animation: dot-blink 1s ease-in-out infinite;' : ''}
2898
+ `;
2899
+ badge.appendChild(dot);
2900
+
2901
+ // LIVE text
2902
+ const text = document.createElement('span');
2903
+ text.textContent = 'LIVE';
2904
+ text.style.cssText = `
2905
+ color: #ffffff;
2906
+ font-size: 11px;
2907
+ font-weight: 800;
2908
+ letter-spacing: 0.5px;
2909
+ `;
2910
+ badge.appendChild(text);
2911
+
2912
+ return badge;
2913
+ }
2914
+
2915
+ private ensureTickerAnimations(): void {
2570
2916
  if (!document.querySelector('#uvf-ticker-animation')) {
2571
2917
  const style = document.createElement('style');
2572
2918
  style.id = 'uvf-ticker-animation';
@@ -2575,11 +2921,21 @@ export class WebPlayer extends BasePlayer {
2575
2921
  0% { transform: translateX(0%); }
2576
2922
  100% { transform: translateX(-100%); }
2577
2923
  }
2924
+ @keyframes globe-rotate {
2925
+ 0% { transform: rotate(0deg); }
2926
+ 100% { transform: rotate(360deg); }
2927
+ }
2928
+ @keyframes live-pulse {
2929
+ 0%, 100% { opacity: 1; transform: scale(1); }
2930
+ 50% { opacity: 0.85; transform: scale(1.02); }
2931
+ }
2932
+ @keyframes dot-blink {
2933
+ 0%, 100% { opacity: 1; }
2934
+ 50% { opacity: 0.3; }
2935
+ }
2578
2936
  `;
2579
2937
  document.head.appendChild(style);
2580
2938
  }
2581
-
2582
- return ticker;
2583
2939
  }
2584
2940
 
2585
2941
  private calculateTickerDuration(config: FlashNewsTickerConfig): number {
@@ -13,5 +13,16 @@ export { WebPlayerView } from './react/WebPlayerView';
13
13
  // Export EPG (Electronic Program Guide) components
14
14
  export * from './react/EPG';
15
15
 
16
+ // Export Flash News Ticker types
17
+ export type {
18
+ FlashNewsTickerConfig,
19
+ FlashNewsTickerItem,
20
+ FlashNewsTickerAPI,
21
+ TickerDisplayConfig,
22
+ TickerStyleVariant,
23
+ BroadcastTheme,
24
+ BroadcastStyleConfig
25
+ } from './react/types/FlashNewsTickerTypes';
26
+
16
27
  // Version
17
28
  export const VERSION = '1.0.0';