unified-video-framework 1.4.158 → 1.4.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/package.json +12 -2
  2. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.d.ts +18 -0
  3. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.d.ts.map +1 -0
  4. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.js +117 -0
  5. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.js.map +1 -0
  6. package/packages/core/dist/analytics/core/AnalyticsProvider.d.ts +18 -0
  7. package/packages/core/dist/analytics/core/AnalyticsProvider.d.ts.map +1 -0
  8. package/packages/core/dist/analytics/core/AnalyticsProvider.js +99 -0
  9. package/packages/core/dist/analytics/core/AnalyticsProvider.js.map +1 -0
  10. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.d.ts +20 -0
  11. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.d.ts.map +1 -0
  12. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.js +161 -0
  13. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.js.map +1 -0
  14. package/packages/core/dist/analytics/core/EventBatcher.d.ts +32 -0
  15. package/packages/core/dist/analytics/core/EventBatcher.d.ts.map +1 -0
  16. package/packages/core/dist/analytics/core/EventBatcher.js +98 -0
  17. package/packages/core/dist/analytics/core/EventBatcher.js.map +1 -0
  18. package/packages/core/dist/analytics/core/PlayerAnalytics.d.ts +19 -0
  19. package/packages/core/dist/analytics/core/PlayerAnalytics.d.ts.map +1 -0
  20. package/packages/core/dist/analytics/core/PlayerAnalytics.js +80 -0
  21. package/packages/core/dist/analytics/core/PlayerAnalytics.js.map +1 -0
  22. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.d.ts +32 -0
  23. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.d.ts.map +1 -0
  24. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.js +220 -0
  25. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.js.map +1 -0
  26. package/packages/core/dist/analytics/index.d.ts +13 -0
  27. package/packages/core/dist/analytics/index.d.ts.map +1 -0
  28. package/packages/core/dist/analytics/index.js +13 -0
  29. package/packages/core/dist/analytics/index.js.map +1 -0
  30. package/packages/core/dist/analytics/types/AnalyticsTypes.d.ts +239 -0
  31. package/packages/core/dist/analytics/types/AnalyticsTypes.d.ts.map +1 -0
  32. package/packages/core/dist/analytics/types/AnalyticsTypes.js +8 -0
  33. package/packages/core/dist/analytics/types/AnalyticsTypes.js.map +1 -0
  34. package/packages/core/dist/analytics/utils/DeviceDetection.d.ts +27 -0
  35. package/packages/core/dist/analytics/utils/DeviceDetection.d.ts.map +1 -0
  36. package/packages/core/dist/analytics/utils/DeviceDetection.js +184 -0
  37. package/packages/core/dist/analytics/utils/DeviceDetection.js.map +1 -0
  38. package/packages/core/dist/chapter-manager.d.ts +39 -0
  39. package/packages/core/dist/index.d.ts +1 -0
  40. package/packages/core/dist/index.d.ts.map +1 -1
  41. package/packages/core/dist/index.js +1 -0
  42. package/packages/core/dist/index.js.map +1 -1
  43. package/packages/core/src/analytics/README.md +902 -0
  44. package/packages/core/src/analytics/adapters/PlayerAnalyticsAdapter.ts +156 -0
  45. package/packages/core/src/analytics/core/AnalyticsProvider.ts +169 -0
  46. package/packages/core/src/analytics/core/DynamicAnalyticsManager.ts +199 -0
  47. package/packages/core/src/analytics/core/EventBatcher.ts +160 -0
  48. package/packages/core/src/analytics/core/PlayerAnalytics.ts +147 -0
  49. package/packages/core/src/analytics/index.ts +51 -0
  50. package/packages/core/src/analytics/types/AnalyticsTypes.ts +315 -0
  51. package/packages/core/src/analytics/utils/DeviceDetection.ts +220 -0
  52. package/packages/core/src/index.ts +3 -0
  53. package/packages/ios/README.md +84 -0
  54. package/packages/web/dist/WebPlayer.d.ts +5 -0
  55. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  56. package/packages/web/dist/WebPlayer.js +255 -63
  57. package/packages/web/dist/WebPlayer.js.map +1 -1
  58. package/packages/web/dist/epg/EPGController.d.ts +78 -0
  59. package/packages/web/dist/epg/EPGController.d.ts.map +1 -0
  60. package/packages/web/dist/epg/EPGController.js +476 -0
  61. package/packages/web/dist/epg/EPGController.js.map +1 -0
  62. package/packages/web/src/WebPlayer.ts +303 -79
  63. package/src/analytics/README.md +902 -0
  64. package/src/analytics/adapters/PlayerAnalyticsAdapter.ts +572 -0
  65. package/src/analytics/core/DynamicAnalyticsManager.ts +526 -0
  66. package/src/analytics/examples/DynamicAnalyticsExample.ts +324 -0
  67. package/src/analytics/index.ts +60 -0
@@ -0,0 +1,572 @@
1
+ /**
2
+ * PlayerAnalyticsAdapter - Adapter for integrating with player analytics API
3
+ * Maps internal analytics events to the player analytics system format
4
+ */
5
+
6
+ import {
7
+ AnalyticsEvent as InternalAnalyticsEvent,
8
+ PlayerState,
9
+ DeviceInfo,
10
+ PlayerSessionInfo,
11
+ AnalyticsConfig
12
+ } from '../types/AnalyticsTypes';
13
+
14
+ // Types for the player analytics system
15
+ export interface PlayerAnalyticsConfig {
16
+ baseUrl: string;
17
+ apiKey: string;
18
+ tenantId?: string;
19
+ playerId: string;
20
+ heartbeatInterval?: number; // seconds, default 10
21
+ batchSize?: number; // default 10
22
+ flushInterval?: number; // seconds, default 30
23
+ }
24
+
25
+ export interface AnalyticsSession {
26
+ sessionId: string;
27
+ playerId: string;
28
+ timestamp: number;
29
+ customData?: Record<string, any>;
30
+ }
31
+
32
+ export interface VideoInfo {
33
+ id: string;
34
+ title?: string;
35
+ type: 'video' | 'series' | 'episode' | 'audio' | 'season' | 'live';
36
+ duration?: number; // seconds
37
+ isLive?: boolean;
38
+ metadata?: Record<string, any>;
39
+ }
40
+
41
+ export interface DeviceData {
42
+ deviceType: 'mobile' | 'tablet' | 'smart_tv' | 'desktop' | 'tv';
43
+ model?: string;
44
+ os?: string;
45
+ osVersion?: string;
46
+ appVersion?: string;
47
+ screen?: { width: number; height: number };
48
+ connection?: {
49
+ effectiveType?: string;
50
+ downlink?: number;
51
+ rtt?: number;
52
+ saveData?: boolean;
53
+ };
54
+ browser?: string;
55
+ }
56
+
57
+ export interface PlayerData {
58
+ playbackRate?: number;
59
+ muted?: boolean;
60
+ volume?: number;
61
+ readyState?: number;
62
+ networkState?: number;
63
+ }
64
+
65
+ export interface EngagementData {
66
+ totalWatchTime?: number; // milliseconds
67
+ completionPercentage?: number; // 0-100
68
+ maxWatchedPosition?: number; // seconds
69
+ seekEvents?: number;
70
+ qualityChanges?: number;
71
+ fullscreenToggles?: number;
72
+ totalBufferingTime?: number; // milliseconds
73
+ }
74
+
75
+ export interface ErrorData {
76
+ code?: string;
77
+ message?: string;
78
+ type?: string;
79
+ }
80
+
81
+ export interface AnalyticsEventData {
82
+ eventType: string;
83
+ timestamp: number;
84
+ currentTime?: number; // seconds
85
+ video?: VideoInfo;
86
+ device?: DeviceData;
87
+ location?: {
88
+ country?: string;
89
+ region?: string;
90
+ city?: string;
91
+ lat?: number;
92
+ lon?: number;
93
+ };
94
+ player?: PlayerData;
95
+ engagement?: EngagementData;
96
+ error?: ErrorData;
97
+ }
98
+
99
+ export interface AnalyticsPayload {
100
+ session: AnalyticsSession;
101
+ events: AnalyticsEventData[];
102
+ }
103
+
104
+ export class PlayerAnalyticsAdapter {
105
+ private config: PlayerAnalyticsConfig;
106
+ private currentSession: AnalyticsSession | null = null;
107
+ private eventQueue: AnalyticsEventData[] = [];
108
+ private engagementTracker: {
109
+ totalWatchTime: number;
110
+ seekEvents: number;
111
+ qualityChanges: number;
112
+ fullscreenToggles: number;
113
+ totalBufferingTime: number;
114
+ maxWatchedPosition: number;
115
+ lastPlayTime?: number;
116
+ bufferingStartTime?: number;
117
+ } = {
118
+ totalWatchTime: 0,
119
+ seekEvents: 0,
120
+ qualityChanges: 0,
121
+ fullscreenToggles: 0,
122
+ totalBufferingTime: 0,
123
+ maxWatchedPosition: 0
124
+ };
125
+ private heartbeatInterval: NodeJS.Timeout | null = null;
126
+ private flushInterval: NodeJS.Timeout | null = null;
127
+
128
+ constructor(config: PlayerAnalyticsConfig) {
129
+ this.config = {
130
+ heartbeatInterval: 10,
131
+ batchSize: 10,
132
+ flushInterval: 30,
133
+ ...config
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Start a new analytics session
139
+ */
140
+ public startSession(videoInfo: {
141
+ id: string;
142
+ title?: string;
143
+ type?: 'video' | 'series' | 'episode' | 'audio' | 'season' | 'live';
144
+ duration?: number;
145
+ isLive?: boolean;
146
+ metadata?: Record<string, any>;
147
+ }, customData?: Record<string, any>): string {
148
+ // End current session if active
149
+ if (this.currentSession) {
150
+ this.endSession();
151
+ }
152
+
153
+ const sessionId = this.generateSessionId();
154
+ this.currentSession = {
155
+ sessionId,
156
+ playerId: this.config.playerId,
157
+ timestamp: Date.now(),
158
+ customData
159
+ };
160
+
161
+ // Reset engagement tracker
162
+ this.engagementTracker = {
163
+ totalWatchTime: 0,
164
+ seekEvents: 0,
165
+ qualityChanges: 0,
166
+ fullscreenToggles: 0,
167
+ totalBufferingTime: 0,
168
+ maxWatchedPosition: 0
169
+ };
170
+
171
+ // Create session_start event
172
+ const sessionStartEvent: AnalyticsEventData = {
173
+ eventType: 'session_start',
174
+ timestamp: Date.now(),
175
+ video: {
176
+ id: videoInfo.id,
177
+ title: videoInfo.title,
178
+ type: videoInfo.type || 'video',
179
+ duration: videoInfo.duration,
180
+ isLive: videoInfo.isLive,
181
+ metadata: videoInfo.metadata
182
+ },
183
+ device: this.mapDeviceInfo()
184
+ };
185
+
186
+ this.queueEvent(sessionStartEvent);
187
+ this.startHeartbeat();
188
+ this.startFlushInterval();
189
+
190
+ return sessionId;
191
+ }
192
+
193
+ /**
194
+ * End the current session
195
+ */
196
+ public async endSession(): Promise<void> {
197
+ if (!this.currentSession) {
198
+ return;
199
+ }
200
+
201
+ // Stop intervals
202
+ if (this.heartbeatInterval) {
203
+ clearInterval(this.heartbeatInterval);
204
+ this.heartbeatInterval = null;
205
+ }
206
+ if (this.flushInterval) {
207
+ clearInterval(this.flushInterval);
208
+ this.flushInterval = null;
209
+ }
210
+
211
+ // Create session_end event with final engagement data
212
+ const sessionEndEvent: AnalyticsEventData = {
213
+ eventType: 'session_end',
214
+ timestamp: Date.now(),
215
+ engagement: { ...this.engagementTracker }
216
+ };
217
+
218
+ this.queueEvent(sessionEndEvent);
219
+
220
+ // Flush remaining events
221
+ await this.flush();
222
+
223
+ this.currentSession = null;
224
+ }
225
+
226
+ /**
227
+ * Track an internal analytics event and convert it to API format
228
+ */
229
+ public trackEvent(internalEvent: InternalAnalyticsEvent, playerState?: PlayerState, videoInfo?: any): void {
230
+ if (!this.currentSession) {
231
+ console.warn('No active analytics session');
232
+ return;
233
+ }
234
+
235
+ const apiEvent = this.convertInternalEvent(internalEvent, playerState, videoInfo);
236
+ if (apiEvent) {
237
+ this.updateEngagementMetrics(apiEvent, playerState);
238
+ this.queueEvent(apiEvent);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Track a custom event directly
244
+ */
245
+ public trackCustomEvent(eventType: string, data: Partial<AnalyticsEventData> = {}): void {
246
+ if (!this.currentSession) {
247
+ console.warn('No active analytics session');
248
+ return;
249
+ }
250
+
251
+ const event: AnalyticsEventData = {
252
+ eventType,
253
+ timestamp: Date.now(),
254
+ ...data
255
+ };
256
+
257
+ this.queueEvent(event);
258
+ }
259
+
260
+ /**
261
+ * Manually flush queued events
262
+ */
263
+ public async flush(): Promise<void> {
264
+ if (this.eventQueue.length === 0) {
265
+ return;
266
+ }
267
+
268
+ const eventsToSend = this.eventQueue.splice(0, this.config.batchSize!);
269
+ const payload: AnalyticsPayload = {
270
+ session: this.currentSession!,
271
+ events: eventsToSend
272
+ };
273
+
274
+ try {
275
+ await this.sendEvents(payload);
276
+ } catch (error) {
277
+ console.error('Failed to send analytics events:', error);
278
+ // Re-queue events for retry (simple strategy)
279
+ this.eventQueue.unshift(...eventsToSend);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Get current session info
285
+ */
286
+ public getCurrentSession(): AnalyticsSession | null {
287
+ return this.currentSession;
288
+ }
289
+
290
+ /**
291
+ * Get engagement metrics
292
+ */
293
+ public getEngagementMetrics(): EngagementData {
294
+ return { ...this.engagementTracker };
295
+ }
296
+
297
+ /**
298
+ * Destroy the adapter and cleanup
299
+ */
300
+ public destroy(): void {
301
+ if (this.currentSession) {
302
+ this.endSession();
303
+ }
304
+ }
305
+
306
+ // Private methods
307
+
308
+ private convertInternalEvent(
309
+ internalEvent: InternalAnalyticsEvent,
310
+ playerState?: PlayerState,
311
+ videoInfo?: any
312
+ ): AnalyticsEventData | null {
313
+ const eventTypeMap: Record<string, string> = {
314
+ 'play': 'play',
315
+ 'pause': 'pause',
316
+ 'ended': 'ended',
317
+ 'seeking': 'seeking',
318
+ 'seeked': 'seeked',
319
+ 'waiting': 'waiting',
320
+ 'stalled': 'stalled',
321
+ 'buffering': 'waiting',
322
+ 'rebuffering': 'stalled',
323
+ 'qualitychange': 'quality_change',
324
+ 'volumechange': 'volume_change',
325
+ 'fullscreenchange': 'fullscreen_change',
326
+ 'error': 'error',
327
+ 'timeupdate': 'heartbeat',
328
+ 'chapterstart': 'custom_chapter_start',
329
+ 'chapterend': 'custom_chapter_end',
330
+ 'chapterskip': 'custom_chapter_skip'
331
+ };
332
+
333
+ const mappedEventType = eventTypeMap[internalEvent.eventType];
334
+ if (!mappedEventType) {
335
+ // Use custom_ prefix for unmapped events
336
+ return {
337
+ eventType: `custom_${internalEvent.eventType}`,
338
+ timestamp: internalEvent.timestamp,
339
+ currentTime: internalEvent.currentTime
340
+ };
341
+ }
342
+
343
+ const apiEvent: AnalyticsEventData = {
344
+ eventType: mappedEventType,
345
+ timestamp: internalEvent.timestamp,
346
+ currentTime: internalEvent.currentTime
347
+ };
348
+
349
+ // Add video info if available
350
+ if (videoInfo) {
351
+ apiEvent.video = {
352
+ id: videoInfo.id || internalEvent.videoId,
353
+ title: videoInfo.title,
354
+ type: videoInfo.type || 'video',
355
+ duration: videoInfo.duration,
356
+ isLive: videoInfo.isLive
357
+ };
358
+ }
359
+
360
+ // Add player state if available
361
+ if (playerState) {
362
+ apiEvent.player = {
363
+ playbackRate: playerState.playbackRate,
364
+ muted: playerState.muted,
365
+ volume: playerState.volume,
366
+ readyState: playerState.readyState,
367
+ networkState: playerState.networkState
368
+ };
369
+ }
370
+
371
+ // Add device info
372
+ apiEvent.device = this.mapDeviceInfo();
373
+
374
+ // Add specific event data
375
+ if (internalEvent.eventType === 'error') {
376
+ const errorEvent = internalEvent as any;
377
+ apiEvent.error = {
378
+ code: errorEvent.errorCode,
379
+ message: errorEvent.errorMessage,
380
+ type: errorEvent.errorType
381
+ };
382
+ }
383
+
384
+ return apiEvent;
385
+ }
386
+
387
+ private mapDeviceInfo(): DeviceData {
388
+ const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : '';
389
+
390
+ let deviceType: DeviceData['deviceType'] = 'desktop';
391
+ if (userAgent.includes('mobile')) deviceType = 'mobile';
392
+ else if (userAgent.includes('tablet') || userAgent.includes('ipad')) deviceType = 'tablet';
393
+ else if (userAgent.includes('smart-tv') || userAgent.includes('tv')) deviceType = 'smart_tv';
394
+
395
+ let os = 'Unknown';
396
+ let osVersion = '';
397
+
398
+ if (userAgent.includes('windows')) {
399
+ os = 'Windows';
400
+ const match = userAgent.match(/windows nt ([\d.]+)/);
401
+ osVersion = match ? match[1] : '';
402
+ } else if (userAgent.includes('mac')) {
403
+ os = 'macOS';
404
+ const match = userAgent.match(/mac os x ([\d_]+)/);
405
+ osVersion = match ? match[1].replace(/_/g, '.') : '';
406
+ } else if (userAgent.includes('linux')) {
407
+ os = 'Linux';
408
+ } else if (userAgent.includes('android')) {
409
+ os = 'Android';
410
+ const match = userAgent.match(/android ([\d.]+)/);
411
+ osVersion = match ? match[1] : '';
412
+ } else if (userAgent.includes('ios') || userAgent.includes('iphone') || userAgent.includes('ipad')) {
413
+ os = 'iOS';
414
+ const match = userAgent.match(/os ([\d_]+)/);
415
+ osVersion = match ? match[1].replace(/_/g, '.') : '';
416
+ }
417
+
418
+ const device: DeviceData = {
419
+ deviceType,
420
+ os,
421
+ osVersion: osVersion || undefined,
422
+ browser: this.getBrowserName(userAgent)
423
+ };
424
+
425
+ // Add screen info if available
426
+ if (typeof window !== 'undefined' && window.screen) {
427
+ device.screen = {
428
+ width: window.screen.width,
429
+ height: window.screen.height
430
+ };
431
+ }
432
+
433
+ // Add connection info if available
434
+ if (typeof navigator !== 'undefined' && 'connection' in navigator) {
435
+ const connection = (navigator as any).connection;
436
+ if (connection) {
437
+ device.connection = {
438
+ effectiveType: connection.effectiveType,
439
+ downlink: connection.downlink,
440
+ rtt: connection.rtt,
441
+ saveData: connection.saveData
442
+ };
443
+ }
444
+ }
445
+
446
+ return device;
447
+ }
448
+
449
+ private getBrowserName(userAgent: string): string {
450
+ if (userAgent.includes('edg/')) return 'Edge';
451
+ if (userAgent.includes('chrome/')) return 'Chrome';
452
+ if (userAgent.includes('firefox/')) return 'Firefox';
453
+ if (userAgent.includes('safari/') && !userAgent.includes('chrome')) return 'Safari';
454
+ if (userAgent.includes('opera/')) return 'Opera';
455
+ return 'Unknown';
456
+ }
457
+
458
+ private updateEngagementMetrics(event: AnalyticsEventData, playerState?: PlayerState): void {
459
+ const now = Date.now();
460
+
461
+ switch (event.eventType) {
462
+ case 'play':
463
+ this.engagementTracker.lastPlayTime = now;
464
+ break;
465
+
466
+ case 'pause':
467
+ if (this.engagementTracker.lastPlayTime) {
468
+ this.engagementTracker.totalWatchTime += now - this.engagementTracker.lastPlayTime;
469
+ this.engagementTracker.lastPlayTime = undefined;
470
+ }
471
+ break;
472
+
473
+ case 'seeking':
474
+ this.engagementTracker.seekEvents++;
475
+ break;
476
+
477
+ case 'quality_change':
478
+ this.engagementTracker.qualityChanges++;
479
+ break;
480
+
481
+ case 'fullscreen_change':
482
+ this.engagementTracker.fullscreenToggles++;
483
+ break;
484
+
485
+ case 'waiting':
486
+ this.engagementTracker.bufferingStartTime = now;
487
+ break;
488
+
489
+ case 'stalled':
490
+ if (this.engagementTracker.bufferingStartTime) {
491
+ this.engagementTracker.totalBufferingTime += now - this.engagementTracker.bufferingStartTime;
492
+ this.engagementTracker.bufferingStartTime = undefined;
493
+ }
494
+ break;
495
+
496
+ case 'heartbeat':
497
+ if (this.engagementTracker.lastPlayTime) {
498
+ this.engagementTracker.totalWatchTime += now - this.engagementTracker.lastPlayTime;
499
+ this.engagementTracker.lastPlayTime = now;
500
+ }
501
+ if (event.currentTime && event.currentTime > this.engagementTracker.maxWatchedPosition) {
502
+ this.engagementTracker.maxWatchedPosition = event.currentTime;
503
+ }
504
+ break;
505
+ }
506
+ }
507
+
508
+ private queueEvent(event: AnalyticsEventData): void {
509
+ this.eventQueue.push(event);
510
+
511
+ // Auto-flush if batch size reached
512
+ if (this.eventQueue.length >= this.config.batchSize!) {
513
+ this.flush().catch(error => {
514
+ console.error('Auto-flush failed:', error);
515
+ });
516
+ }
517
+ }
518
+
519
+ private async sendEvents(payload: AnalyticsPayload): Promise<void> {
520
+ const response = await fetch(`${this.config.baseUrl}/analytics/player/ingest`, {
521
+ method: 'POST',
522
+ headers: {
523
+ 'Content-Type': 'application/json',
524
+ 'X-API-Key': this.config.apiKey,
525
+ ...(this.config.tenantId && { 'X-Tenant-ID': this.config.tenantId })
526
+ },
527
+ body: JSON.stringify(payload)
528
+ });
529
+
530
+ if (!response.ok) {
531
+ throw new Error(`Analytics API error: ${response.status} ${response.statusText}`);
532
+ }
533
+ }
534
+
535
+ private startHeartbeat(): void {
536
+ if (this.heartbeatInterval) {
537
+ clearInterval(this.heartbeatInterval);
538
+ }
539
+
540
+ this.heartbeatInterval = setInterval(() => {
541
+ if (this.currentSession) {
542
+ const heartbeatEvent: AnalyticsEventData = {
543
+ eventType: 'heartbeat',
544
+ timestamp: Date.now(),
545
+ device: this.mapDeviceInfo()
546
+ };
547
+ this.queueEvent(heartbeatEvent);
548
+ }
549
+ }, this.config.heartbeatInterval! * 1000);
550
+ }
551
+
552
+ private startFlushInterval(): void {
553
+ if (this.flushInterval) {
554
+ clearInterval(this.flushInterval);
555
+ }
556
+
557
+ this.flushInterval = setInterval(() => {
558
+ this.flush().catch(error => {
559
+ console.error('Scheduled flush failed:', error);
560
+ });
561
+ }, this.config.flushInterval! * 1000);
562
+ }
563
+
564
+ private generateSessionId(): string {
565
+ return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
566
+ }
567
+ }
568
+
569
+ // Factory function
570
+ export function createPlayerAnalyticsAdapter(config: PlayerAnalyticsConfig): PlayerAnalyticsAdapter {
571
+ return new PlayerAnalyticsAdapter(config);
572
+ }