unified-video-framework 1.4.159 → 1.4.161
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 -2
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +224 -27
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +224 -27
- package/src/analytics/README.md +0 -902
- package/src/analytics/adapters/PlayerAnalyticsAdapter.ts +0 -572
- package/src/analytics/core/DynamicAnalyticsManager.ts +0 -526
- package/src/analytics/examples/DynamicAnalyticsExample.ts +0 -324
- package/src/analytics/index.ts +0 -60
|
@@ -1,572 +0,0 @@
|
|
|
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
|
-
}
|