UNPKG

17.4 kBJavaScriptView Raw
1/*!
2 * YouTube tracking for Snowplow v3.2.3 (http://bit.ly/sp-js)
3 * Copyright 2022 Snowplow Analytics Ltd
4 * Licensed under BSD-3-Clause
5 */
6
7import { __spreadArray, __assign } from 'tslib';
8import { dispatchToTrackersInCollection } from '@snowplow/browser-tracker-core';
9import { buildSelfDescribingEvent } from '@snowplow/tracker-core';
10
11var SnowplowEvent;
12(function (SnowplowEvent) {
13 SnowplowEvent["PERCENTPROGRESS"] = "percentprogress";
14 SnowplowEvent["SEEK"] = "seek";
15 SnowplowEvent["VOLUMECHANGE"] = "volumechange";
16})(SnowplowEvent || (SnowplowEvent = {}));
17
18var YouTubeIFrameAPIURL = 'https://www.youtube.com/iframe_api';
19// The payload a YouTube player event emits has no identifier of what event it is
20// Some payloads can emit the same data
21// i.e. onError and onPlaybackRateChange can both emit '{data: 2}'
22var YTPlayerEvent;
23(function (YTPlayerEvent) {
24 YTPlayerEvent["ONSTATECHANGE"] = "onStateChange";
25 YTPlayerEvent["ONPLAYBACKQUALITYCHANGE"] = "onPlaybackQualityChange";
26 YTPlayerEvent["ONERROR"] = "onError";
27 YTPlayerEvent["ONAPICHANGE"] = "onApiChange";
28 YTPlayerEvent["ONPLAYBACKRATECHANGE"] = "onPlaybackRateChange";
29 YTPlayerEvent["ONREADY"] = "onReady";
30})(YTPlayerEvent || (YTPlayerEvent = {}));
31var YTStateEvent = {
32 '-1': 'unstarted',
33 '0': 'ended',
34 '1': 'play',
35 '2': 'pause',
36 '3': 'buffering',
37 '5': 'cued'
38};
39var CaptureEventToYouTubeEvent = {
40 ready: YTPlayerEvent.ONREADY,
41 playbackratechange: YTPlayerEvent.ONPLAYBACKRATECHANGE,
42 playbackqualitychange: YTPlayerEvent.ONPLAYBACKQUALITYCHANGE,
43 error: YTPlayerEvent.ONERROR,
44 apichange: YTPlayerEvent.ONAPICHANGE
45};
46// As every state event requires YTPlayerEvent.ONSTATECHANGE, they are added
47// to CaptureEventToYouTubeEvent with the below loop
48Object.keys(YTStateEvent).forEach(function (k) { return (CaptureEventToYouTubeEvent[YTStateEvent[k]] = YTPlayerEvent.ONSTATECHANGE); });
49var YTState;
50(function (YTState) {
51 YTState["UNSTARTED"] = "unstarted";
52 YTState["ENDED"] = "ended";
53 YTState["PLAYING"] = "play";
54 YTState["PAUSED"] = "pause";
55 YTState["BUFFERING"] = "buffering";
56 YTState["CUED"] = "cued";
57})(YTState || (YTState = {}));
58var YTError = {
59 2: 'INVALID_URL',
60 5: 'HTML5_ERROR',
61 100: 'VIDEO_NOT_FOUND',
62 101: 'MISSING_EMBED_PERMISSION',
63 150: 'MISSING_EMBED_PERMISSION'
64};
65
66var YTEvent;
67(function (YTEvent) {
68 YTEvent["STATECHANGE"] = "statechange";
69 YTEvent["PLAYBACKQUALITYCHANGE"] = "playbackqualitychange";
70 YTEvent["ERROR"] = "error";
71 YTEvent["APICHANGE"] = "apichange";
72 YTEvent["PLAYBACKRATECHANGE"] = "playbackratechange";
73 YTEvent["READY"] = "ready";
74})(YTEvent || (YTEvent = {}));
75[
76 YTState.BUFFERING,
77 YTState.CUED,
78 YTState.ENDED,
79 YTState.PAUSED,
80 YTState.PLAYING,
81 YTState.UNSTARTED,
82];
83
84var AllEvents = __spreadArray(__spreadArray(__spreadArray([], Object.keys(YTEvent).map(function (k) { return YTEvent[k]; })), Object.keys(SnowplowEvent).map(function (k) { return SnowplowEvent[k]; })), Object.keys(YTState).map(function (k) { return YTState[k]; }));
85var DefaultEvents = [
86 YTState.PAUSED,
87 YTState.PLAYING,
88 YTState.ENDED,
89 SnowplowEvent.SEEK,
90 SnowplowEvent.VOLUMECHANGE,
91 YTPlayerEvent.ONPLAYBACKQUALITYCHANGE,
92 YTPlayerEvent.ONPLAYBACKRATECHANGE,
93 SnowplowEvent.PERCENTPROGRESS,
94];
95var EventGroups = {
96 AllEvents: AllEvents,
97 DefaultEvents: DefaultEvents
98};
99
100function trackingOptionsParser(mediaId, conf) {
101 var defaults = {
102 mediaId: mediaId,
103 captureEvents: DefaultEvents,
104 youtubeEvents: [
105 YTPlayerEvent.ONSTATECHANGE,
106 YTPlayerEvent.ONPLAYBACKQUALITYCHANGE,
107 YTPlayerEvent.ONERROR,
108 YTPlayerEvent.ONPLAYBACKRATECHANGE,
109 ],
110 updateRate: 500,
111 progress: {
112 boundaries: [10, 25, 50, 75],
113 boundaryTimeoutIds: []
114 }
115 };
116 if (!conf)
117 return defaults;
118 if (conf.updateRate)
119 defaults.updateRate = conf.updateRate;
120 if (conf.captureEvents) {
121 var parsedEvents = [];
122 var _loop_1 = function (ev) {
123 // If an event is an EventGroup, get the events from that group
124 if (EventGroups.hasOwnProperty(ev)) {
125 parsedEvents = parsedEvents.concat(EventGroups[ev]);
126 }
127 else if (!Object.keys(AllEvents).filter(function (k) { return k === ev; })) {
128 console.warn("'" + ev + "' is not a valid event.");
129 }
130 else {
131 parsedEvents.push(ev);
132 }
133 };
134 for (var _i = 0, _a = conf.captureEvents; _i < _a.length; _i++) {
135 var ev = _a[_i];
136 _loop_1(ev);
137 }
138 conf.captureEvents = parsedEvents;
139 for (var _b = 0, _c = conf.captureEvents; _b < _c.length; _b++) {
140 var ev = _c[_b];
141 var youtubeEvent = CaptureEventToYouTubeEvent[ev];
142 if (CaptureEventToYouTubeEvent.hasOwnProperty(ev) && defaults.youtubeEvents.indexOf(youtubeEvent) === -1) {
143 defaults.youtubeEvents.push(youtubeEvent);
144 }
145 }
146 if (conf.captureEvents.indexOf(SnowplowEvent.PERCENTPROGRESS) !== -1) {
147 defaults.progress = {
148 boundaries: (conf === null || conf === void 0 ? void 0 : conf.boundaries) || defaults.progress.boundaries,
149 boundaryTimeoutIds: []
150 };
151 }
152 }
153 return __assign(__assign({}, defaults), conf);
154}
155// URLSearchParams is not supported in IE
156// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
157function addUrlParam(url, key, value) {
158 var urlParams = parseUrlParams(url);
159 urlParams[key] = value;
160 return url + "?" + urlParamsToString(urlParams);
161}
162function parseUrlParams(url) {
163 var params = {};
164 var urlParams = url.split('?')[1];
165 if (!urlParams)
166 return params;
167 urlParams.split('&').forEach(function (p) {
168 var param = p.split('=');
169 params[param[0]] = param[1];
170 });
171 return params;
172}
173function urlParamsToString(urlParams) {
174 // convert an object of url parameters to a string
175 var params = '';
176 Object.keys(urlParams).forEach(function (p) {
177 params += p + "=" + urlParams[p] + "&";
178 });
179 return params.slice(0, -1);
180}
181
182function buildYouTubeEvent(player, eventName, conf, eventData) {
183 var data = { type: eventName };
184 if (conf.hasOwnProperty('label'))
185 data.label = conf.label;
186 var context = [
187 getYouTubeEntities(player, conf.urlParameters, eventData),
188 getMediaPlayerEntities(eventName, player, conf.urlParameters, eventData),
189 ];
190 return {
191 schema: 'iglu:com.snowplowanalytics.snowplow/media_player_event/jsonschema/1-0-0',
192 data: data,
193 context: context
194 };
195}
196function getYouTubeEntities(player, urlParameters, eventData) {
197 var spherical = player.getSphericalProperties();
198 var playerStates = {
199 buffering: false,
200 cued: false,
201 unstarted: false
202 };
203 var state = player.getPlayerState();
204 if (playerStates.hasOwnProperty(YTStateEvent[state])) {
205 playerStates[YTStateEvent[state]] = true;
206 }
207 var data = {
208 autoPlay: urlParameters.autoplay === '1',
209 avaliablePlaybackRates: player.getAvailablePlaybackRates(),
210 buffering: playerStates.buffering,
211 controls: urlParameters.controls !== '0',
212 cued: playerStates.cued,
213 loaded: parseInt(String(player.getVideoLoadedFraction() * 100)),
214 playbackQuality: player.getPlaybackQuality(),
215 playerId: player.getIframe().id,
216 unstarted: playerStates.unstarted,
217 url: player.getVideoUrl()
218 };
219 if (spherical)
220 data = __assign(__assign({}, data), spherical);
221 if (eventData === null || eventData === void 0 ? void 0 : eventData.error)
222 data.error = eventData.error;
223 var playlistIndex = player.getPlaylistIndex();
224 if (playlistIndex !== -1)
225 data.playlistIndex = playlistIndex;
226 var playlist = player.getPlaylist();
227 if (playlist) {
228 data.playlist = playlist.map(function (item) { return parseInt(item); });
229 }
230 var qualityLevels = player.getAvailableQualityLevels();
231 if (qualityLevels)
232 data.avaliableQualityLevels = qualityLevels;
233 return {
234 schema: 'iglu:com.youtube/youtube/jsonschema/1-0-0',
235 data: data
236 };
237}
238function getMediaPlayerEntities(e, player, urlParameters, eventData) {
239 var playerStates = {
240 ended: false,
241 paused: false
242 };
243 var state = player.getPlayerState();
244 if (playerStates.hasOwnProperty(YTStateEvent[state])) {
245 playerStates[YTStateEvent[state]] = true;
246 }
247 var data = {
248 currentTime: player.getCurrentTime(),
249 duration: player.getDuration(),
250 ended: playerStates.ended,
251 loop: urlParameters.loop === '1',
252 muted: player.isMuted(),
253 paused: playerStates.paused,
254 playbackRate: player.getPlaybackRate(),
255 volume: player.getVolume()
256 };
257 if (e === SnowplowEvent.PERCENTPROGRESS) {
258 data.percentProgress = eventData.percentThrough;
259 }
260 return {
261 schema: 'iglu:com.snowplowanalytics.snowplow/media_player/jsonschema/1-0-0',
262 data: data
263 };
264}
265
266var _trackers = {};
267var trackedPlayers = {};
268var trackingQueue = [];
269var LOG;
270function YouTubeTrackingPlugin() {
271 return {
272 activateBrowserPlugin: function (tracker) {
273 _trackers[tracker.id] = tracker;
274 },
275 logger: function (logger) {
276 LOG = logger;
277 }
278 };
279}
280function trackEvent(event, trackers) {
281 if (trackers === void 0) { trackers = Object.keys(_trackers); }
282 dispatchToTrackersInCollection(trackers, _trackers, function (t) {
283 t.core.track(buildSelfDescribingEvent({ event: event }), event.context, event.timestamp);
284 });
285}
286function enableYouTubeTracking(args) {
287 var conf = trackingOptionsParser(args.id, args.options);
288 var el = document.getElementById(args.id);
289 if (!el) {
290 LOG.error('Cannot find YouTube iframe');
291 return;
292 }
293 // The 'enablejsapi' parameter is required to be '1' for the API to be able to communicate with the player
294 if (el.src.indexOf('enablejsapi') === -1) {
295 el.src = addUrlParam(el.src, 'enablejsapi', '1');
296 }
297 conf.urlParameters = parseUrlParams(el.src);
298 // If the API is ready, we can immediately add the listeners
299 if (typeof YT !== 'undefined' && typeof YT.Player !== 'undefined') {
300 addListeners(conf);
301 }
302 else {
303 // If not, we put them into a queue that will have listeners added once the API is ready
304 // and start trying to load the iframe API
305 trackingQueue.push(conf);
306 handleYouTubeIframeAPI();
307 }
308}
309var iframeAPIRetryWait = 100;
310function handleYouTubeIframeAPI() {
311 // First we check if the script tag exists in the DOM, and enable the API if not
312 var scriptTags = Array.prototype.slice.call(document.getElementsByTagName('script'));
313 if (!scriptTags.some(function (s) { return s.src === YouTubeIFrameAPIURL; })) {
314 // Load the Iframe API
315 // https://developers.google.com/youtube/iframe_api_reference
316 var tag = document.createElement('script');
317 tag.src = YouTubeIFrameAPIURL;
318 var firstScriptTag = document.getElementsByTagName('script')[0];
319 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
320 }
321 // Once the API is ready to use, 'YT.Player' will be defined
322 // 'YT.Player' is not available immediately after 'YT' is defined,
323 // so we need to wait until 'YT' is defined to then check 'YT.Player'
324 if (typeof YT === 'undefined' || typeof YT.Player === 'undefined') {
325 if (iframeAPIRetryWait <= 6400) {
326 setTimeout(handleYouTubeIframeAPI, iframeAPIRetryWait);
327 iframeAPIRetryWait *= 2;
328 }
329 else {
330 LOG.error('YouTube iframe API failed to load.');
331 }
332 }
333 else {
334 // Once the API is avaliable, listeners are attached to anything sitting in the queue
335 while (trackingQueue.length) {
336 addListeners(trackingQueue.pop());
337 }
338 }
339}
340function addListeners(conf) {
341 var _a;
342 var builtInEvents = (_a = {},
343 _a[YTPlayerEvent.ONREADY] = function () { return youtubeEvent(trackedPlayers[conf.mediaId].player, YTEvent.READY, conf); },
344 _a[YTPlayerEvent.ONSTATECHANGE] = function (e) {
345 if (conf.captureEvents.indexOf(YTStateEvent[e.data.toString()]) !== -1) {
346 youtubeEvent(trackedPlayers[conf.mediaId].player, YTStateEvent[e.data], conf);
347 }
348 },
349 _a[YTPlayerEvent.ONPLAYBACKQUALITYCHANGE] = function () {
350 return youtubeEvent(trackedPlayers[conf.mediaId].player, YTEvent.PLAYBACKQUALITYCHANGE, conf);
351 },
352 _a[YTPlayerEvent.ONAPICHANGE] = function () { return youtubeEvent(trackedPlayers[conf.mediaId].player, YTEvent.APICHANGE, conf); },
353 _a[YTPlayerEvent.ONERROR] = function (e) {
354 return youtubeEvent(trackedPlayers[conf.mediaId].player, YTEvent.ERROR, conf, { error: YTError[e.data] });
355 },
356 _a[YTPlayerEvent.ONPLAYBACKRATECHANGE] = function () {
357 return youtubeEvent(trackedPlayers[conf.mediaId].player, YTEvent.PLAYBACKRATECHANGE, conf);
358 },
359 _a);
360 var playerEvents = {};
361 conf.youtubeEvents.forEach(function (e) {
362 playerEvents[e] = builtInEvents[e];
363 });
364 trackedPlayers[conf.mediaId] = {
365 player: new YT.Player(conf.mediaId, { events: __assign({}, playerEvents) }),
366 conf: conf,
367 seekTracking: {
368 prevTime: 0,
369 enabled: false
370 },
371 volumeTracking: {
372 prevVolume: 0,
373 enabled: false
374 }
375 };
376}
377function youtubeEvent(player, eventName, conf, eventData) {
378 var playerInstance = trackedPlayers[conf.mediaId];
379 if (!playerInstance.seekTracking.enabled && conf.captureEvents.indexOf('seek') !== 1) {
380 enableSeekTracking(player, conf, eventData);
381 }
382 if (!playerInstance.volumeTracking.enabled && conf.captureEvents.indexOf('volume') !== 1) {
383 enableVolumeTracking(player, conf, eventData);
384 }
385 if (conf.hasOwnProperty('boundaries')) {
386 progressHandler(player, eventName, conf);
387 }
388 var event = buildYouTubeEvent(player, eventName, conf, eventData);
389 trackEvent(event);
390}
391// Progress Tracking
392function progressHandler(player, eventName, conf) {
393 var timeoutIds = conf.progress.boundaryTimeoutIds;
394 if (eventName === YTState.PAUSED) {
395 timeoutIds.forEach(function (id) { return clearTimeout(id); });
396 timeoutIds.length = 0;
397 }
398 if (eventName === YTState.PLAYING) {
399 setPercentageBoundTimeouts(player, conf);
400 }
401}
402function setPercentageBoundTimeouts(player, conf) {
403 var _a;
404 var currentTime = player.getCurrentTime();
405 (_a = conf.progress) === null || _a === void 0 ? void 0 : _a.boundaries.forEach(function (p) {
406 var _a;
407 var percentTime = player.getDuration() * 1000 * (p / 100);
408 if (currentTime !== 0) {
409 percentTime -= currentTime * 1000;
410 }
411 if (p < percentTime) {
412 (_a = conf.progress) === null || _a === void 0 ? void 0 : _a.boundaryTimeoutIds.push(setTimeout(function () { return waitAnyRemainingTimeAfterTimeout(player, conf, percentTime, p); }, percentTime));
413 }
414 });
415}
416// The timeout in setPercentageBoundTimeouts fires ~100 - 300ms early
417// waitAnyRemainingTimeAfterTimeout ensures the event is fired accurately
418function waitAnyRemainingTimeAfterTimeout(player, conf, percentTime, p) {
419 if (player.getCurrentTime() * 1000 < percentTime) {
420 setTimeout(function () { return waitAnyRemainingTimeAfterTimeout(player, conf, percentTime, p); }, 10);
421 }
422 else {
423 youtubeEvent(player, SnowplowEvent.PERCENTPROGRESS, conf, {
424 percentThrough: p
425 });
426 }
427}
428// Seek Tracking
429function enableSeekTracking(player, conf, eventData) {
430 trackedPlayers[conf.mediaId].seekTracking.enabled = true;
431 setInterval(function () { return seekEventTracker(player, conf, eventData); }, conf.updateRate);
432}
433function seekEventTracker(player, conf, eventData) {
434 var playerInstance = trackedPlayers[conf.mediaId];
435 var playerTime = player.getCurrentTime();
436 if (Math.abs(playerTime - (playerInstance.seekTracking.prevTime + 0.5)) > 1) {
437 youtubeEvent(player, SnowplowEvent.SEEK, conf, eventData);
438 }
439 playerInstance.seekTracking.prevTime = playerTime;
440}
441// Volume Tracking
442function enableVolumeTracking(player, conf, eventData) {
443 trackedPlayers[conf.mediaId].volumeTracking.enabled = true;
444 trackedPlayers[conf.mediaId].volumeTracking.prevVolume = player.getVolume();
445 setInterval(function () { return volumeEventTracker(player, conf, eventData); }, conf.updateRate);
446}
447function volumeEventTracker(player, conf, eventData) {
448 var playerVolumeTracking = trackedPlayers[conf.mediaId].volumeTracking;
449 var playerVolume = player.getVolume();
450 if (playerVolume !== playerVolumeTracking.prevVolume) {
451 youtubeEvent(player, SnowplowEvent.VOLUMECHANGE, conf, eventData);
452 }
453 playerVolumeTracking.prevVolume = playerVolume;
454}
455
456export { YouTubeTrackingPlugin, enableYouTubeTracking };
457//# sourceMappingURL=index.module.js.map