-
Notifications
You must be signed in to change notification settings - Fork 6
/
ConvivaAnalytics.ts
436 lines (375 loc) · 17.1 KB
/
ConvivaAnalytics.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
import * as Conviva from '@convivainc/conviva-js-coresdk';
import {
AdBreakEvent,
AdEvent,
AudioChangedEvent,
ErrorEvent,
PlaybackEvent,
PlayerAPI,
PlayerEvent,
PlayerEventBase,
SeekEvent,
TimeShiftEvent,
VideoQualityChangedEvent,
SubtitleEvent,
} from 'bitmovin-player';
import { Metadata } from './ContentMetadataBuilder';
import { ObjectUtils } from './helper/ObjectUtils';
import { ConvivaAnalyticsConfiguration, ConvivaAnalyticsTracker, EventAttributes } from './ConvivaAnalyticsTracker';
import { ConvivaAnalyticsSsai } from './ConvivaAnalyticsSsai';
import { PlayerEventWrapper } from './helper/PlayerEventWrapper';
import { AdHelper } from './helper/AdHelper';
import { Html5Logging } from './Html5Logging';
export class ConvivaAnalytics {
private handlers?: PlayerEventWrapper;
private readonly convivaAnalyticsTracker: ConvivaAnalyticsTracker;
private _player?: PlayerAPI;
private get player(): PlayerAPI {
if (!this._player) {
throw new Error('Player is not initialized, either pass it to the constructor or attach it via `attachPlayer` before using the integration.');
}
return this._player;
}
private readonly debugLoggingEnabled: boolean;
/**
* Tracks the last ad break event to get the ad position and other ad break related information
* in the ad started event to report it to Conviva.
*/
private lastAdBreakEvent: AdBreakEvent;
private convivaSsaiAnalytics: ConvivaAnalyticsSsai;
private readonly logger: Conviva.LoggingInterface = new Html5Logging();
/**
* Tracks the duration of main content. Needed as the player may return the ad duration instead.
*/
private mainContentDuration = 0;
public readonly ssai: Omit<ConvivaAnalyticsSsai, 'reset'>;
constructor(player: PlayerAPI | undefined, customerKey: string, config: ConvivaAnalyticsConfiguration = {}) {
this.convivaAnalyticsTracker = new ConvivaAnalyticsTracker(customerKey, config);
this.debugLoggingEnabled = config.debugLoggingEnabled || false;
this._player = player;
if (player) {
this.attachPlayer(player);
}
this.convivaSsaiAnalytics = new ConvivaAnalyticsSsai(this.convivaAnalyticsTracker);
// Do not expose `reset` method to the public API.
this.ssai = {
get isAdBreakActive() {
return this.convivaSsaiAnalytics.isAdBreakActive;
},
reportAdBreakStarted: this.convivaSsaiAnalytics.reportAdBreakStarted.bind(this.convivaSsaiAnalytics),
reportAdStarted: this.convivaSsaiAnalytics.reportAdStarted.bind(this.convivaSsaiAnalytics),
reportAdFinished: this.convivaSsaiAnalytics.reportAdFinished.bind(this.convivaSsaiAnalytics),
reportAdSkipped: this.convivaSsaiAnalytics.reportAdSkipped.bind(this.convivaSsaiAnalytics),
reportAdBreakFinished: this.convivaSsaiAnalytics.reportAdBreakFinished.bind(this.convivaSsaiAnalytics),
};
}
/**
* Attaches the player instance to the integration. This can be used for late player attaching e.g.
* to measure VST (Video start time) more precisely.
*
* Has no effect if there is already a `Player` instance set. Use the `new ConvivaAnalytics(...)` without `player`
* if you plan to attach a `Player` instance later in the life-cycle.
*
* Example:
* ```
* const convivaAnalytics = new ConvivaAnalytics(undefined, '<CONVIVA_CUSTOMER_KEY>')
*
* // Asset name is required to be set when initializing the session before `player.load()`.
* convivaAnalytics.updateContentMetadata({ assetName: 'My video' });
* convivaAnalytics.initializeSession();
*
* // ... Additional setup steps
*
* convivaAnalytics.attachPlayer(player);
* player.load({ ... });
* ```
*
* @param player The player instance to attach to the integration.
*/
public attachPlayer(player: PlayerAPI): void {
const {canAttach, reason} = this.convivaAnalyticsTracker.canAttachPlayer(player);
if (!canAttach) {
this.logger.consoleLog(
`[ ConvivaAnalyticsTracker ] cannot attach player: ${reason}`,
Conviva.SystemSettings.LogLevel.WARNING,
);
return;
}
this.convivaAnalyticsTracker.attachPlayer(player);
this._player = player;
this.handlers = new PlayerEventWrapper(player);
this.registerPlayerEvents();
}
/**
* Initializes a new conviva tracking session.
*
* Warning: The integration can only be validated without external session managing. So when using this method we can
* no longer ensure that the session is managed at the correct time. Additional: Since some metadata attributes
* relies on the players source we can't ensure that all metadata attributes are present at session creation.
* Therefore it could be that there will be a 'ContentMetadata created late' issue after conviva validation.
*
* If no source was loaded and no assetName was set via updateContentMetadata this method will throw an error.
*/
public initializeSession(): void {
this.convivaAnalyticsTracker.initializeSession();
}
/**
* Ends the current conviva tracking session. If there an ad break is active it will also report the ad as skipped.
* Results in a no-opt if there is no active session.
*
* Warning: Sessions will no longer be created automatically after this method has been called.
*
* The integration can only be validated without external session managing. So when using this method we can
* no longer ensure that the session is managed at the correct time.
*/
public endSession(): void {
this.debugLog('[ ConvivaAnalytics ] external ending session');
this.convivaSsaiAnalytics.reset();
this.convivaAnalyticsTracker.endSession();
}
/**
* Sends a custom application-level event to Conviva's Player Insight. An application-level event can always
* be sent and is not tied to a specific video.
* @param eventName arbitrary event name
* @param eventAttributes a string-to-string dictionary object with arbitrary attribute keys and values
*/
public sendCustomApplicationEvent(eventName: string, eventAttributes: EventAttributes = {}): void {
this.convivaAnalyticsTracker.sendCustomApplicationEvent(eventName, eventAttributes);
}
/**
* Sends a custom playback-level event to Conviva's Player Insight. A playback-level event can only be sent
* during an active video session.
* @param eventName arbitrary event name
* @param eventAttributes a string-to-string dictionary object with arbitrary attribute keys and values
*/
public sendCustomPlaybackEvent(eventName: string, eventAttributes: EventAttributes = {}): void {
this.convivaAnalyticsTracker.sendCustomPlaybackEvent(eventName, eventAttributes);
}
/**
* Will update the contentMetadata which are tracked with conviva.
*
* If there is an active session only permitted values will be updated and propagated immediately.
* If there is no active session the values will set on session creation.
*
* Attributes set via this method will override automatic tracked once.
* @param metadataOverrides Metadata attributes which will be used to track to conviva.
* @see ContentMetadataBuilder for more information about permitted attributes
*/
public updateContentMetadata(metadataOverrides: Partial<Metadata>) {
this.convivaAnalyticsTracker.updateContentMetadata(metadataOverrides);
}
/**
* Sends a custom deficiency event during playback to Conviva's Player Insight. If no session is active it will NOT
* create one.
*
* @param message Message which will be send to conviva
* @param severity One of FATAL or WARNING
* @param endSession Boolean flag if session should be closed after reporting the deficiency (Default: true)
*/
public reportPlaybackDeficiency(
message: string,
severity: Conviva.valueof<Conviva.ConvivaConstants['ErrorSeverity']>,
endSession: boolean = true,
) {
this.convivaAnalyticsTracker.reportPlaybackDeficiency(message, severity, endSession);
}
/**
* Puts the session state in a notMonitored state.
*/
public pauseTracking(): void {
this.convivaAnalyticsTracker.pauseTracking();
}
/**
* Puts the session state from a notMonitored state into the last one tracked.
*/
public resumeTracking(): void {
this.convivaAnalyticsTracker.resumeTracking();
}
public release(): void {
this.releaseInternal();
}
private releaseInternal(event?: PlayerEventBase): void {
const isPlayerDestroyed = event?.type === PlayerEvent.Destroy;
if (!isPlayerDestroyed) {
this.unregisterPlayerEvents();
}
this._player = null;
this.handlers = null;
this.debugLog('[ ConvivaAnalytics ] releasing', {
event,
isPlayerDestroyed,
});
this.convivaAnalyticsTracker.release(isPlayerDestroyed);
this.convivaSsaiAnalytics.reset();
this.lastAdBreakEvent = null;
}
private debugLog(message?: any, ...optionalParams: any[]): void {
if (this.debugLoggingEnabled) {
console.log.apply(console, arguments);
}
}
private onPlaybackStateChanged = (event: PlayerEventBase) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] playback state change related event', event);
this.convivaAnalyticsTracker.trackPlaybackStateFromEvent(event);
};
private onPlaying = (event: PlaybackEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] playing', event);
this.onPlaybackStateChanged(event);
};
private onVideoQualityChanged = (event: VideoQualityChangedEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] video quality changed', event);
this.convivaAnalyticsTracker.trackVideoQualityChanged(event);
};
private onCustomEvent = (event: PlayerEventBase) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] custom playback related event', event);
const eventAttributes = ObjectUtils.flatten(event);
this.sendCustomPlaybackEvent(event.type, eventAttributes);
};
private onAdBreakStarted = (event: AdBreakEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] adbreak started', event);
this.lastAdBreakEvent = event;
this.convivaAnalyticsTracker.trackAdBreakStarted(Conviva.Constants.AdType.CLIENT_SIDE);
this.convivaAnalyticsTracker.trackPlaybackStateFromEvent(event);
};
private onAdStarted = (event: AdEvent) => {
if (!this.lastAdBreakEvent) {
this.debugLog('[ ConvivaAnalytics ] received ad started without active ad break', event);
return;
}
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] ad started', event);
const adInfo = AdHelper.extractCsaiConvivaAdInfo(this.lastAdBreakEvent, this.mainContentDuration, event);
const bitrateKbps = event.ad.data?.bitrate;
this.convivaAnalyticsTracker.trackAdStarted(adInfo, Conviva.Constants.AdType.CLIENT_SIDE, bitrateKbps);
// No need to call reportPlaybackStateFromEvent as this is covered by `trackAdStarted`
}
private onAdFinished = (event: AdEvent) => {
if (!this.lastAdBreakEvent) {
this.debugLog('[ ConvivaAnalytics ] received ad finished without active ad break', event);
return;
}
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] ad finished', event);
this.convivaAnalyticsTracker.trackAdFinished();
this.convivaAnalyticsTracker.trackPlaybackStateFromEvent(event);
}
private onAdSkipped = (event: AdEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] ad skipped', event);
this.convivaAnalyticsTracker.trackAdSkipped();
this.onCustomEvent(event);
};
private onRestoringContent = (event: PlayerEventBase) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] restoring content', event);
this.convivaAnalyticsTracker.trackRestoringContent();
this.convivaAnalyticsTracker.trackPlaybackStateFromEvent(event);
};
private onAdBreakFinished = (event: AdBreakEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] adbreak finished', event);
this.convivaAnalyticsTracker.trackAdBreakFinished(Conviva.Constants.AdType.CLIENT_SIDE);
// No need to call reportPlaybackStateFromEvent as this is covered by `trackAdBreakFinished`
}
private onAdError = (event: ErrorEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] ad error', event);
this.convivaAnalyticsTracker.trackAdError(event);
this.onCustomEvent(event);
};
private onSeek = (event: SeekEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] seek', event);
this.convivaAnalyticsTracker.trackSeekStart(event.seekTarget);
};
private onSeeked = (event: SeekEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] seeked', event);
this.convivaAnalyticsTracker.trackSeekEnd();
this.onPlaybackStateChanged(event);
};
private onTimeShift = (event: TimeShiftEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] time shift', event);
// According to conviva it is valid to pass -1 for seeking in live streams
this.convivaAnalyticsTracker.trackSeekStart(-1);
};
private onTimeShifted = (event: TimeShiftEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] time shifted', event);
this.convivaAnalyticsTracker.trackSeekEnd();
this.onPlaybackStateChanged(event);
};
private onAudioChanged = (event: AudioChangedEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] audio changed', event);
this.convivaAnalyticsTracker.trackUpdateAudioTrack(event.targetAudio);
};
private onSubtitleEnabled = (event: SubtitleEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] subtitled enabled', event);
this.convivaAnalyticsTracker.trackUpdateSubtitleTrack(event.subtitle);
};
private onSubtitleDisabled = (event: SubtitleEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] subtitles disabled', event);
this.convivaAnalyticsTracker.trackTurnOffSubtitles();
};
private onError = (event: ErrorEvent) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] error', event);
this.convivaAnalyticsTracker.trackError(event);
};
private onDestroy = (event: any) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] destroy', event);
this.releaseInternal(event);
};
private onSourceLoaded = (event: PlayerEventBase) => {
this.debugLog('[ ConvivaAnalytics ] [ Player Event ] onSourceLoaded', event);
this.mainContentDuration = this.player.getDuration();
};
private static readonly stallTrackingStartEvents = [
PlayerEvent.Play,
PlayerEvent.Seek,
PlayerEvent.TimeShift,
];
private static readonly stallTrackingClearEvents = [
PlayerEvent.StallStarted, // StallStarted is reported as BUFFERING immediately. Does not need the delayed timeout approach.
PlayerEvent.Playing,
PlayerEvent.Paused,
PlayerEvent.Seeked,
PlayerEvent.TimeShifted,
PlayerEvent.StallEnded,
PlayerEvent.PlaybackFinished,
PlayerEvent.AdStarted,
];
private registerPlayerEvents(): void {
this.handlers.add(PlayerEvent.SourceLoaded, this.onSourceLoaded);
this.handlers.add(PlayerEvent.Playing, this.onPlaying);
this.handlers.add(PlayerEvent.Paused, this.onPlaybackStateChanged);
this.handlers.add(PlayerEvent.StallStarted, this.onPlaybackStateChanged);
this.handlers.add(PlayerEvent.StallEnded, this.onPlaybackStateChanged);
this.handlers.add(PlayerEvent.VideoPlaybackQualityChanged, this.onVideoQualityChanged);
this.handlers.add(PlayerEvent.AudioPlaybackQualityChanged, this.onCustomEvent);
this.handlers.add(PlayerEvent.Muted, this.onCustomEvent);
this.handlers.add(PlayerEvent.Unmuted, this.onCustomEvent);
this.handlers.add(PlayerEvent.ViewModeChanged, this.onCustomEvent);
this.handlers.add(PlayerEvent.AdStarted, this.onAdStarted);
this.handlers.add(PlayerEvent.AdFinished, this.onAdFinished);
this.handlers.add(PlayerEvent.AdBreakStarted, this.onAdBreakStarted);
this.handlers.add(PlayerEvent.RestoringContent, this.onRestoringContent);
this.handlers.add(PlayerEvent.AdBreakFinished, this.onAdBreakFinished);
this.handlers.add(PlayerEvent.AdSkipped, this.onAdSkipped);
this.handlers.add(PlayerEvent.AdError, this.onAdError);
this.handlers.add(PlayerEvent.Error, this.onError);
this.handlers.add(PlayerEvent.Destroy, this.onDestroy);
this.handlers.add(PlayerEvent.Seek, this.onSeek);
this.handlers.add(PlayerEvent.Seeked, this.onSeeked);
this.handlers.add(PlayerEvent.TimeShift, this.onTimeShift);
this.handlers.add(PlayerEvent.TimeShifted, this.onTimeShifted);
this.handlers.add(PlayerEvent.AudioChanged, this.onAudioChanged);
this.handlers.add(PlayerEvent.SubtitleEnabled, this.onSubtitleEnabled);
this.handlers.add(PlayerEvent.SubtitleDisabled, this.onSubtitleDisabled);
this.handlers.add(PlayerEvent.CastStarted, this.onCustomEvent);
this.handlers.add(PlayerEvent.CastStopped, this.onCustomEvent);
ConvivaAnalytics.stallTrackingStartEvents.forEach((eventName) => {
this.handlers.add(eventName, (event) => {
this.convivaAnalyticsTracker.startStallTrackingTimeout(event);
});
});
ConvivaAnalytics.stallTrackingClearEvents.forEach((eventName) => {
this.handlers.add(eventName, (event) => {
this.convivaAnalyticsTracker.clearStallTrackingTimeout(event);
});
});
}
private unregisterPlayerEvents(): void {
this.handlers?.clear();
}
}