forked from snowplow/snowplow-javascript-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmediaTracking.ts
More file actions
208 lines (188 loc) · 6.89 KB
/
Copy pathmediaTracking.ts
File metadata and controls
208 lines (188 loc) · 6.89 KB
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
import { LOG, SelfDescribingJson } from '@snowplow/tracker-core';
import { MediaAdTracking } from './adTracking';
import { buildMediaPlayerEntity, buildMediaPlayerEvent } from './core';
import { MediaPingInterval } from './pingInterval';
import { MediaSessionTracking } from './sessionTracking';
import {
MediaPlayer,
MediaAdUpdate,
MediaPlayerAdBreakUpdate,
MediaPlayerUpdate,
MediaEventType,
MediaEvent,
} from './types';
/**
* Manages the state and built-in entities for a media tracking that starts when a user
* call `startMediaTracking` and ends with `endMediaTracking`.
*
* It updates the internal state for each tracked event and returns context entities with
* properties updated based on the internal state.
*/
export class MediaTracking {
/// ID of the media tracking that is used to refer to it by the user.
id: string;
/// Percentage boundaries when to track percent progress events.
private boundaries?: number[];
/// List of boundaries for which percent progress events were already sent to avoid sending again.
private sentBoundaries: number[] = [];
/// State for the media player context entity that is updated as new events are tracked.
player: MediaPlayer = {
currentTime: 0,
paused: true,
ended: false,
};
/// Used to add media player session entity.
private session?: MediaSessionTracking;
/// Tracks ping events independently but stored here to stop when media tracking stops.
private pingInterval?: MediaPingInterval;
/// Manages ad entities.
private adTracking = new MediaAdTracking();
/// Used to prevent tracking seek start events multiple times.
private isSeeking = false;
/// Context entities to attach to all events
private customContext?: Array<SelfDescribingJson>;
/// Optional list of event types to allow tracking and discard others.
private captureEvents?: MediaEventType[];
// Whether to update page activity when playing media. Enabled by default.
private updatePageActivityWhilePlaying?: boolean;
constructor(
id: string,
player?: MediaPlayerUpdate,
session?: MediaSessionTracking,
pingInterval?: MediaPingInterval,
boundaries?: number[],
captureEvents?: MediaEventType[],
updatePageActivityWhilePlaying?: boolean,
context?: Array<SelfDescribingJson>
) {
this.id = id;
this.updatePlayer(player);
this.session = session;
this.pingInterval = pingInterval;
this.boundaries = boundaries;
this.captureEvents = captureEvents;
this.updatePageActivityWhilePlaying = updatePageActivityWhilePlaying;
this.customContext = context;
// validate event names in the captureEvents list
captureEvents?.forEach((eventType) => {
if (!Object.values(MediaEventType).includes(eventType)) {
LOG.warn('Unknown media event ' + eventType);
}
});
}
/**
* Called when user calls `endMediaTracking()`.
*/
stop() {
this.pingInterval?.clear();
}
/**
* Updates the internal state given the new event or new media player info and returns events to track.
* @param eventType Type of the event tracked or undefined when only updating player properties.
* @param player Updates to the media player stored entity.
* @param ad Updates to the ad entity.
* @param adBreak Updates to the ad break entity.
* @returns List of events with entities to track.
*/
update(
mediaEvent?: MediaEvent,
customEvent?: SelfDescribingJson,
player?: MediaPlayerUpdate,
ad?: MediaAdUpdate,
adBreak?: MediaPlayerAdBreakUpdate
): { event: SelfDescribingJson; context: SelfDescribingJson[] }[] {
// update state
this.updatePlayer(player);
if (mediaEvent !== undefined) {
this.adTracking.updateForThisEvent(mediaEvent.type, this.player, ad, adBreak);
}
this.session?.update(mediaEvent?.type, this.player, this.adTracking.adBreak);
this.pingInterval?.update(this.player);
// build context entities
let context = [buildMediaPlayerEntity(this.player)];
if (this.session !== undefined) {
context.push(this.session.getContext());
}
if (this.customContext) {
context = context.concat(this.customContext);
}
context = context.concat(this.adTracking.getContext());
// build event types to track
const mediaEventsToTrack: MediaEvent[] = [];
if (mediaEvent !== undefined && this.shouldTrackEvent(mediaEvent.type)) {
mediaEventsToTrack.push(mediaEvent);
}
if (this.shouldSendPercentProgress()) {
mediaEventsToTrack.push({
type: MediaEventType.PercentProgress,
eventBody: { percentProgress: this.getPercentProgress() },
});
}
// update state for events after this one
if (mediaEvent !== undefined) {
this.adTracking.updateForNextEvent(mediaEvent.type);
}
const eventsToTrack = mediaEventsToTrack.map((event) => {
return { event: buildMediaPlayerEvent(event), context: context };
});
if (customEvent !== undefined) {
eventsToTrack.push({ event: customEvent, context: context });
}
return eventsToTrack;
}
shouldUpdatePageActivity(): boolean {
return (this.updatePageActivityWhilePlaying ?? true) && !this.player.paused;
}
private updatePlayer(player?: MediaPlayerUpdate) {
if (player !== undefined) {
this.player = {
...this.player,
...player,
};
}
}
private shouldSendPercentProgress(): boolean {
const percentProgress = this.getPercentProgress();
if (this.boundaries === undefined || percentProgress === undefined || this.player.paused) {
return false;
}
let boundaries = this.boundaries.filter((b) => b <= (percentProgress ?? 0));
if (boundaries.length == 0) {
return false;
}
let boundary = Math.max(...boundaries);
if (!this.sentBoundaries.includes(boundary)) {
this.sentBoundaries.push(boundary);
return true;
}
return false;
}
private shouldTrackEvent(eventType: MediaEventType): boolean {
return this.updateSeekingAndCheckIfShouldTrack(eventType) && this.allowedToCaptureEventType(eventType);
}
/** Prevents multiple seek start events to be tracked after each other without a seek end (happens when scrubbing). */
private updateSeekingAndCheckIfShouldTrack(eventType: MediaEventType): boolean {
if (eventType == MediaEventType.SeekStart) {
if (this.isSeeking) {
return false;
}
this.isSeeking = true;
} else if (eventType == MediaEventType.SeekEnd) {
this.isSeeking = false;
}
return true;
}
private allowedToCaptureEventType(eventType: MediaEventType): boolean {
return this.captureEvents === undefined || this.captureEvents.includes(eventType);
}
private getPercentProgress(): number | undefined {
if (
this.player.duration === null ||
this.player.duration === undefined ||
this.player.duration == 0
) {
return undefined;
}
return Math.floor(((this.player.currentTime ?? 0) / this.player.duration) * 100);
}
}