@newrelic/video-core 3.1.0 → 3.2.0-beta-0

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.
@@ -0,0 +1,1060 @@
1
+ import Log from "./log";
2
+ import Tracker from "./tracker";
3
+ import TrackerState from "./videotrackerstate";
4
+ import pkg from "../package.json";
5
+
6
+ /**
7
+ * Base video tracker class provides extensible tracking over video elements. See {@link Tracker}.
8
+ * Extend this class to create your own video tracker class. Override getter methods and
9
+ * registerListeners/unregisterListeners to provide full integration with your video experience.
10
+ *
11
+ * @example
12
+ * Tracker instances should be added to Core library to start sending data:
13
+ * nrvideo.Core.addTracker(new Tracker())
14
+ *
15
+ * @extends Tracker
16
+ */
17
+ class VideoTracker extends Tracker {
18
+ /**
19
+ * Constructor, receives player and options.
20
+ * Lifecycle: constructor > {@link setOptions} > {@link setPlayer} > {@link registerListeners}.
21
+ *
22
+ * @param {Object} [player] Player to track. See {@link setPlayer}.
23
+ * @param {Object} [options] Options for the tracker. See {@link setOptions}.
24
+ */
25
+ constructor(player, options) {
26
+ super();
27
+
28
+ /**
29
+ * TrackerState instance. Stores the state of the view. Tracker will automatically update the
30
+ * state of its instance, so there's no need to modify/interact with it manually.
31
+ * @type TrackerState
32
+ */
33
+ this.state = new TrackerState();
34
+
35
+ /**
36
+ * Another Tracker instance to track ads.
37
+ * @type Tracker
38
+ */
39
+ this.adsTracker = null;
40
+
41
+ /**
42
+ * Last bufferType value.
43
+ * @private
44
+ */
45
+ this._lastBufferType = null;
46
+ this._userId = null;
47
+
48
+ options = options || {};
49
+ this.setOptions(options);
50
+ if (player) this.setPlayer(player, options.tag);
51
+
52
+ Log.notice(
53
+ "Tracker " +
54
+ this.getTrackerName() +
55
+ " v" +
56
+ this.getTrackerVersion() +
57
+ " is ready."
58
+ );
59
+ }
60
+
61
+ /* user can set the user Id */
62
+
63
+ setUserId(userId) {
64
+ this._userId = userId;
65
+ }
66
+
67
+ /**
68
+ * Set options for the Tracker.
69
+ *
70
+ * @param {Object} [options] Options for the tracker.
71
+ * @param {Boolean} [options.isAd] True if the tracker is tracking ads. See {@link setIsAd}.
72
+ * @param {number} [options.heartbeat] Set time between heartbeats. See {@link heartbeat}.
73
+ * @param {Object} [options.customData] Set custom data. See {@link customData}.
74
+ * @param {Tracker} [options.parentTracker] Set parent tracker. See {@link parentTracker}.
75
+ * @param {Tracker} [options.adsTracker] Set ads tracker. See {@link adsTracker}.
76
+ * @param {Object} [options.tag] DOM element to track. See {@link setPlayer}.
77
+ */
78
+ setOptions(options) {
79
+ if (options) {
80
+ if (options.adsTracker) {
81
+ this.setAdsTracker(options.adsTracker);
82
+ }
83
+ if (typeof options.isAd === "boolean") {
84
+ this.setIsAd(options.isAd);
85
+ }
86
+ Tracker.prototype.setOptions.apply(this, arguments);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Set a player and/or a tag. If there was one already defined, it will call dispose() first.
92
+ * Will call this.registerListeners() afterwards.
93
+ *
94
+ * @param {Object|string} player New player to save as this.player. If a string is passed,
95
+ * document.getElementById will be called.
96
+ * @param {DOMObject|string} [tag] Optional DOMElement to save as this.tag. If a string is passed,
97
+ * document.getElementById will be called.
98
+ */
99
+
100
+ setPlayer(player, tag) {
101
+ if (this.player || this.tag) this.dispose();
102
+
103
+ if (typeof document !== "undefined" && document.getElementById) {
104
+ if (typeof player === "string") player = document.getElementById(player);
105
+ if (typeof tag === "string") tag = document.getElementById(tag);
106
+ }
107
+
108
+ tag = tag || player; // if no tag is passed, use player as both.
109
+
110
+ this.player = player;
111
+ this.tag = tag;
112
+ this.registerListeners();
113
+ }
114
+
115
+ /** Returns true if the tracker is currently on ads. */
116
+ isAd() {
117
+ return this.state.isAd();
118
+ }
119
+
120
+ /** Sets if the tracker is currenlty tracking ads */
121
+ setIsAd(isAd) {
122
+ this.state.setIsAd(isAd);
123
+ }
124
+
125
+ /**
126
+ * Use this function to set up a child ad tracker. You will be able to access it using
127
+ * this.adsTracker.
128
+ *
129
+ * @param {Tracker} tracker Ad tracker to add
130
+ */
131
+ setAdsTracker(tracker) {
132
+ this.disposeAdsTracker(); // dispose current one
133
+ if (tracker) {
134
+ this.adsTracker = tracker;
135
+ this.adsTracker.setIsAd(true);
136
+ this.adsTracker.parentTracker = this;
137
+ this.adsTracker.on("*", funnelAdEvents.bind(this));
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Dispose current adsTracker.
143
+ */
144
+ disposeAdsTracker() {
145
+ if (this.adsTracker) {
146
+ this.adsTracker.off("*", funnelAdEvents);
147
+ this.adsTracker.dispose();
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Prepares tracker to dispose. Calls unregisterListener and drops references to player and tag.
153
+ */
154
+ dispose() {
155
+ this.stopHeartbeat();
156
+ this.disposeAdsTracker();
157
+ this.unregisterListeners();
158
+ this.player = null;
159
+ this.tag = null;
160
+ }
161
+
162
+ /**
163
+ * Override this method to register listeners to player/tag.
164
+ * @example
165
+ * class SpecificTracker extends Tracker {
166
+ * registerListeners() {
167
+ * this.player.on('play', () => this.playHandler)
168
+ * }
169
+ *
170
+ * playHandler() {
171
+ * this.send(VideoTracker.Events.REQUESTED)
172
+ * }
173
+ * }
174
+ */
175
+ registerListeners() {}
176
+
177
+ /**
178
+ * Override this method to unregister listeners to player/tag created in registerListeners
179
+ * @example
180
+ * class SpecificTracker extends Tracker {
181
+ * registerListeners() {
182
+ * this.player.on('play', () => this.playHandler)
183
+ * }
184
+ *
185
+ * unregisterListeners() {
186
+ * this.player.off('play', () => this.playHandler)
187
+ * }
188
+ *
189
+ * playHandler() {
190
+ * this.send(VideoTracker.Events.REQUESTED)
191
+ * }
192
+ * }
193
+ */
194
+ unregisterListeners() {}
195
+
196
+ /**
197
+ * Trackers will generate unique id's for every new video iteration. If you have your own unique
198
+ * view value, you can override this method to return it.
199
+ * If the tracker has a parentTracker defined, parent viewId will be used.
200
+ */
201
+ getViewId() {
202
+ if (this.parentTracker) {
203
+ return this.parentTracker.getViewId();
204
+ } else {
205
+ return this.state.getViewId();
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Trackers will generate unique id's for every new video session. If you have your own unique
211
+ * view value, you can override this method to return it.
212
+ * If the tracker has a parentTracker defined, parent viewId will be used.
213
+ */
214
+ getViewSession() {
215
+ if (this.parentTracker) {
216
+ return this.parentTracker.getViewSession();
217
+ } else {
218
+ return this.state.getViewSession();
219
+ }
220
+ }
221
+
222
+ /** Override to return the Id of the video. */
223
+ getVideoId() {
224
+ return null;
225
+ }
226
+
227
+ /** Override to return Title of the video. */
228
+ getTitle() {
229
+ return null;
230
+ }
231
+
232
+ /** Override to return True if the video is live. */
233
+ isLive() {
234
+ return null;
235
+ }
236
+
237
+ /** Override to return Bitrate (in bits) of the video. */
238
+ getBitrate() {
239
+ return null;
240
+ }
241
+
242
+ /** Calculates consumed bitrate using webkitVideoDecodedByteCount. */
243
+ getWebkitBitrate() {
244
+ if (this.tag && this.tag.webkitVideoDecodedByteCount) {
245
+ let bitrate;
246
+ if (this._lastWebkitBitrate) {
247
+ bitrate = this.tag.webkitVideoDecodedByteCount;
248
+ let delta = bitrate - this._lastWebkitBitrate;
249
+ let seconds = this.getHeartbeat() / 1000;
250
+ bitrate = Math.round((delta / seconds) * 8);
251
+ }
252
+ this._lastWebkitBitrate = this.tag.webkitVideoDecodedByteCount;
253
+ return bitrate || null;
254
+ }
255
+ }
256
+
257
+ /** Override to return Name of the rendition (ie: 1080p). */
258
+ getRenditionName() {
259
+ return null;
260
+ }
261
+
262
+ /** Override to return Target Bitrate of the rendition. */
263
+ getRenditionBitrate() {
264
+ return null;
265
+ }
266
+
267
+ /**
268
+ * This method will return 'up', 'down' or null depending on if the bitrate of the rendition
269
+ * have changed from the last time it was called.
270
+ *
271
+ * @param {boolean} [saveNewRendition=false] If true, current rendition will be stored to be used
272
+ * the next time this method is called. This allows you to call this.getRenditionShift() without
273
+ * saving the current rendition and thus preventing interferences with RENDITION_CHANGE events.
274
+ */
275
+ getRenditionShift(saveNewRendition) {
276
+ let current = this.getRenditionBitrate();
277
+ let last;
278
+ if (this.isAd()) {
279
+ last = this._lastAdRendition;
280
+ if (saveNewRendition) this._lastAdRendition = current;
281
+ } else {
282
+ last = this._lastRendition;
283
+ if (saveNewRendition) this._lastRendition = current;
284
+ }
285
+
286
+ if (!current || !last) {
287
+ return null;
288
+ } else {
289
+ if (current > last) {
290
+ return "up";
291
+ } else if (current < last) {
292
+ return "down";
293
+ } else {
294
+ return null;
295
+ }
296
+ }
297
+ }
298
+
299
+ /** Override to return renidtion actual Height (before re-scaling). */
300
+ getRenditionHeight() {
301
+ return this.tag ? this.tag.videoHeight : null;
302
+ }
303
+
304
+ /** Override to return rendition actual Width (before re-scaling). */
305
+ getRenditionWidth() {
306
+ return this.tag ? this.tag.videoWidth : null;
307
+ }
308
+
309
+ /** Override to return Duration of the video, in ms. */
310
+ getDuration() {
311
+ return this.tag ? this.tag.duration : null;
312
+ }
313
+
314
+ /** Override to return Playhead (currentTime) of the video, in ms. */
315
+ getPlayhead() {
316
+ return this.tag ? this.tag.currentTime : null;
317
+ }
318
+
319
+ /**
320
+ * Override to return Language of the video. We recommend using locale notation, ie: en_US.
321
+ * {@see https://gist.github.com/jacobbubu/1836273}
322
+ */
323
+ getLanguage() {
324
+ return null;
325
+ }
326
+
327
+ /** Override to return URL of the resource being played. */
328
+ getSrc() {
329
+ return this.tag ? this.tag.currentSrc : null;
330
+ }
331
+
332
+ /** Override to return Playrate (speed) of the video. ie: 1.0, 0.5, 1.25... */
333
+ getPlayrate() {
334
+ return this.tag ? this.tag.playbackRate : null;
335
+ }
336
+
337
+ /** Override to return True if the video is currently muted. */
338
+ isMuted() {
339
+ return this.tag ? this.tag.muted : null;
340
+ }
341
+
342
+ /** Override to return True if the video is currently fullscreen. */
343
+ isFullscreen() {
344
+ return null;
345
+ }
346
+
347
+ /** Override to return the CDN serving the content. */
348
+ getCdn() {
349
+ return null;
350
+ }
351
+
352
+ /** Override to return the Name of the player. */
353
+ getPlayerName() {
354
+ return this.getTrackerName();
355
+ }
356
+
357
+ /** Override to return the Version of the player. */
358
+ getPlayerVersion() {
359
+ return pkg.version;
360
+ }
361
+
362
+ /** Override to return current FPS (Frames per second). */
363
+ getFps() {
364
+ return null;
365
+ }
366
+
367
+ /**
368
+ * Override to return if the player was autoplayed. By default: this.tag.autoplay
369
+ */
370
+ isAutoplayed() {
371
+ return this.tag ? this.tag.autoplay : null;
372
+ }
373
+
374
+ /**
375
+ * Override to return the player preload attribute. By default: this.tag.preload
376
+ */
377
+ getPreload() {
378
+ return this.tag ? this.tag.preload : null;
379
+ }
380
+
381
+ // Only for ads
382
+ /**
383
+ * Override to return Quartile of the ad. 0 before first, 1 after first quartile, 2 after
384
+ * midpoint, 3 after third quartile, 4 when completed.
385
+ */
386
+ getAdQuartile() {
387
+ return null;
388
+ }
389
+
390
+ /**
391
+ * Override to return the position of the ad. Use {@link Constants.AdPositions} enum
392
+ * to fill this data.
393
+ */
394
+ getAdPosition() {
395
+ if (this.parentTracker) {
396
+ return this.parentTracker.state.isStarted ? "mid" : "pre";
397
+ } else {
398
+ return null;
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Override to return the ad partner. ie: ima, freewheel...
404
+ */
405
+ getAdPartner() {
406
+ return null;
407
+ }
408
+
409
+ /**
410
+ * Override to return the creative id of the ad.
411
+ */
412
+ getAdCreativeId() {
413
+ return null;
414
+ }
415
+
416
+ /**
417
+ * Override to return the instrumentation of the player.
418
+ */
419
+
420
+ getInstrumentationProvider() {
421
+ return null;
422
+ }
423
+
424
+ getInstrumentationName() {
425
+ return null;
426
+ }
427
+
428
+ getInstrumentationVersion() {
429
+ return null;
430
+ }
431
+
432
+ /**
433
+ * Do NOT override. This method fills all the appropiate attributes for tracked video.
434
+ *
435
+ * @param {object} [att] Collection of key value attributes
436
+ * @return {object} Filled attributes
437
+ * @final
438
+ */
439
+ getAttributes(att, type) {
440
+ att = Tracker.prototype.getAttributes.apply(this, arguments);
441
+
442
+ if (typeof att.isAd === "undefined") att.isAd = this.isAd();
443
+
444
+ att.viewSession = this.getViewSession();
445
+ att.viewId = this.getViewId();
446
+ att.playerName = this.getPlayerName();
447
+ att.playerVersion = this.getPlayerVersion();
448
+ att["instrumentation.provider"] = this.getInstrumentationProvider();
449
+ att["instrumentation.name"] = this.getInstrumentationName();
450
+ att["instrumentation.version"] = this.getInstrumentationVersion();
451
+ att["enduser.id"] = this._userId;
452
+ att["src"] = "Browser";
453
+
454
+ if (type === "customAction") return att;
455
+
456
+ try {
457
+ att.pageUrl = window.location.href;
458
+ } catch (err) {
459
+ /* skip */
460
+ }
461
+
462
+ if (this.isAd()) {
463
+ // Ads
464
+ att.adId = this.getVideoId();
465
+ att.adTitle = this.getTitle();
466
+ att.adSrc = this.getSrc();
467
+ att.adCdn = this.getCdn();
468
+ att.adBitrate =
469
+ this.getBitrate() ||
470
+ this.getWebkitBitrate() ||
471
+ this.getRenditionBitrate();
472
+ att.adRenditionName = this.getRenditionName();
473
+ att.adRenditionBitrate = this.getRenditionBitrate();
474
+ att.adRenditionHeight = this.getRenditionHeight();
475
+ att.adRenditionWidth = this.getRenditionWidth();
476
+ att.adDuration = this.getDuration();
477
+ att.adPlayhead = this.getPlayhead();
478
+ att.adLanguage = this.getLanguage();
479
+ att.adIsMuted = this.isMuted();
480
+ att.adFps = this.getFps();
481
+ // ad exclusives
482
+ //att.adQuartile = this.getAdQuartile();
483
+ att.adPosition = this.getAdPosition();
484
+ att.adCreativeId = this.getAdCreativeId();
485
+ att.adPartner = this.getAdPartner();
486
+ } else {
487
+ // no ads
488
+ att.contentId = this.getVideoId();
489
+ att.contentTitle = this.getTitle();
490
+ att.contentSrc = this.getSrc();
491
+ att.contentCdn = this.getCdn();
492
+ att.contentPlayhead = this.getPlayhead();
493
+
494
+ att.contentIsLive = this.isLive();
495
+ att.contentBitrate =
496
+ this.getBitrate() ||
497
+ this.getWebkitBitrate() ||
498
+ this.getRenditionBitrate();
499
+ att.contentRenditionName = this.getRenditionName();
500
+ att.contentRenditionBitrate = this.getRenditionBitrate();
501
+ att.contentRenditionHeight = this.getRenditionHeight();
502
+ att.contentRenditionWidth = this.getRenditionWidth();
503
+ att.contentDuration = this.getDuration();
504
+
505
+ att.contentLanguage = this.getLanguage();
506
+ att.contentPlayrate = this.getPlayrate();
507
+ att.contentIsFullscreen = this.isFullscreen();
508
+ att.contentIsMuted = this.isMuted();
509
+ att.contentIsAutoplayed = this.isAutoplayed();
510
+ att.contentPreload = this.getPreload();
511
+ att.contentFps = this.getFps();
512
+
513
+ if (
514
+ this.adsTracker != null &&
515
+ this.adsTracker.state.totalAdPlaytime > 0
516
+ ) {
517
+ att.totalAdPlaytime = this.adsTracker.state.totalAdPlaytime;
518
+ }
519
+ }
520
+
521
+ this.state.getStateAttributes(att);
522
+
523
+ for (let key in this.customData) {
524
+ att[key] = this.customData[key];
525
+ }
526
+
527
+ return att;
528
+ }
529
+
530
+ /**
531
+ * Sends custom event and registers a timeSince attribute.
532
+ * @param {Object} [actionName] Custom action name.
533
+ * @param {Object} [timeSinceAttName] Custom timeSince attribute name.
534
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
535
+ */
536
+ sendCustom(actionName, timeSinceAttName, att) {
537
+ att = att || {};
538
+ this.sendVideoCustomAction(actionName, att);
539
+ this.state.setTimeSinceAttribute(timeSinceAttName);
540
+ }
541
+
542
+ /**
543
+ * Sends associated event and changes view state. An internal state machine will prevent
544
+ * duplicated events. Should be associated to an event using registerListeners.
545
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
546
+ */
547
+ sendPlayerReady(att) {
548
+ if (this.state.goPlayerReady()) {
549
+ att = att || {};
550
+ this.sendVideoAction(VideoTracker.Events.PLAYER_READY, att);
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Sends associated event and changes view state. An internal state machine will prevent
556
+ * duplicated events. Should be associated to an event using registerListeners. Calls
557
+ * {@link startHeartbeat}.
558
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
559
+ */
560
+ sendRequest(att) {
561
+ if (this.state.goRequest()) {
562
+ let ev;
563
+ if (this.isAd()) {
564
+ ev = VideoTracker.Events.AD_REQUEST;
565
+ this.sendVideoAdAction(ev, att);
566
+ } else {
567
+ ev = VideoTracker.Events.CONTENT_REQUEST;
568
+ this.sendVideoAction(ev, att);
569
+ }
570
+
571
+ // this.startHeartbeat();
572
+ // this.state.goHeartbeat();
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Sends associated event and changes view state. An internal state machine will prevent
578
+ * duplicated events. Should be associated to an event using registerListeners.
579
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
580
+ */
581
+ sendStart(att) {
582
+ if (this.state.goStart()) {
583
+ let ev;
584
+ if (this.isAd()) {
585
+ ev = VideoTracker.Events.AD_START;
586
+ if (this.parentTracker) this.parentTracker.state.isPlaying = false;
587
+ this.sendVideoAdAction(ev, att);
588
+ } else {
589
+ ev = VideoTracker.Events.CONTENT_START;
590
+ this.sendVideoAction(ev, att);
591
+ }
592
+ //this.send(ev, att);
593
+ this.startHeartbeat();
594
+ this.state.goHeartbeat();
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Sends associated event and changes view state. An internal state machine will prevent
600
+ * duplicated events. Should be associated to an event using registerListeners. Calls
601
+ * {@link stopHeartbeat}.
602
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
603
+ */
604
+ sendEnd(att) {
605
+ if (this.state.goEnd()) {
606
+ att = att || {};
607
+ let ev;
608
+ if (this.isAd()) {
609
+ ev = VideoTracker.Events.AD_END;
610
+ att.timeSinceAdRequested = this.state.timeSinceRequested.getDeltaTime();
611
+ att.timeSinceAdStarted = this.state.timeSinceStarted.getDeltaTime();
612
+ if (this.parentTracker) this.parentTracker.state.isPlaying = true;
613
+ } else {
614
+ ev = VideoTracker.Events.CONTENT_END;
615
+ att.timeSinceRequested = this.state.timeSinceRequested.getDeltaTime();
616
+ att.timeSinceStarted = this.state.timeSinceStarted.getDeltaTime();
617
+ }
618
+ this.stopHeartbeat();
619
+ //this.send(ev, att);
620
+ this.isAd()
621
+ ? this.sendVideoAdAction(ev, att)
622
+ : this.sendVideoAction(ev, att);
623
+
624
+ if (this.parentTracker && this.isAd())
625
+ this.parentTracker.state.goLastAd();
626
+ this.state.goViewCountUp();
627
+ this.state.totalPlaytime = 0;
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Sends associated event and changes view state. An internal state machine will prevent
633
+ * duplicated events. Should be associated to an event using registerListeners.
634
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
635
+ */
636
+ sendPause(att) {
637
+ if (this.state.goPause()) {
638
+ let ev = this.isAd()
639
+ ? VideoTracker.Events.AD_PAUSE
640
+ : VideoTracker.Events.CONTENT_PAUSE;
641
+
642
+ this.isAd()
643
+ ? this.sendVideoAdAction(ev, att)
644
+ : this.sendVideoAction(ev, att);
645
+
646
+ //this.send(ev, att);
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Sends associated event and changes view state. An internal state machine will prevent
652
+ * duplicated events. Should be associated to an event using registerListeners.
653
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
654
+ */
655
+ sendResume(att) {
656
+ if (this.state.goResume()) {
657
+ att = att || {};
658
+ let ev;
659
+ if (this.isAd()) {
660
+ ev = VideoTracker.Events.AD_RESUME;
661
+ att.timeSinceAdPaused = this.state.timeSincePaused.getDeltaTime();
662
+ } else {
663
+ ev = VideoTracker.Events.CONTENT_RESUME;
664
+ att.timeSincePaused = this.state.timeSincePaused.getDeltaTime();
665
+ }
666
+ //this.send(ev, att);
667
+ this.isAd()
668
+ ? this.sendVideoAdAction(ev, att)
669
+ : this.sendVideoAction(ev, att);
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Sends associated event and changes view state. An internal state machine will prevent
675
+ * duplicated events. Should be associated to an event using registerListeners.
676
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
677
+ */
678
+ sendBufferStart(att) {
679
+ if (this.state.goBufferStart()) {
680
+ att = att || {};
681
+ let ev;
682
+ if (this.isAd()) {
683
+ ev = VideoTracker.Events.AD_BUFFER_START;
684
+ } else {
685
+ ev = VideoTracker.Events.CONTENT_BUFFER_START;
686
+ }
687
+
688
+ att = this.buildBufferAttributes(att);
689
+ this._lastBufferType = att.bufferType;
690
+
691
+ //this.send(ev, att);
692
+ this.isAd()
693
+ ? this.sendVideoAdAction(ev, att)
694
+ : this.sendVideoAction(ev, att);
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Sends associated event and changes view state. An internal state machine will prevent
700
+ * duplicated events. Should be associated to an event using registerListeners.
701
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
702
+ */
703
+ sendBufferEnd(att) {
704
+ if (this.state.goBufferEnd()) {
705
+ att = att || {};
706
+ let ev;
707
+ if (this.isAd()) {
708
+ ev = VideoTracker.Events.AD_BUFFER_END;
709
+ att.timeSinceAdBufferBegin =
710
+ this.state.timeSinceBufferBegin.getDeltaTime();
711
+ } else {
712
+ ev = VideoTracker.Events.CONTENT_BUFFER_END;
713
+ att.timeSinceBufferBegin =
714
+ this.state.timeSinceBufferBegin.getDeltaTime();
715
+ }
716
+
717
+ att = this.buildBufferAttributes(att);
718
+ // Set the bufferType attribute of the last BUFFER_START
719
+ if (this._lastBufferType != null) {
720
+ att.bufferType = this._lastBufferType;
721
+ }
722
+
723
+ // this.send(ev, att);
724
+ this.isAd()
725
+ ? this.sendVideoAdAction(ev, att)
726
+ : this.sendVideoAction(ev, att);
727
+ this.state.initialBufferingHappened = true;
728
+ }
729
+ }
730
+
731
+ buildBufferAttributes(att) {
732
+ if (att.timeSinceStarted == undefined || att.timeSinceStarted < 100) {
733
+ att.isInitialBuffering = !this.state.initialBufferingHappened;
734
+ } else {
735
+ att.isInitialBuffering = false;
736
+ }
737
+
738
+ att.bufferType = this.state.calculateBufferType(att.isInitialBuffering);
739
+
740
+ att.timeSinceResumed = this.state.timeSinceResumed.getDeltaTime();
741
+ att.timeSinceSeekEnd = this.state.timeSinceSeekEnd.getDeltaTime();
742
+
743
+ return att;
744
+ }
745
+
746
+ /**
747
+ * Sends associated event and changes view state. An internal state machine will prevent
748
+ * duplicated events. Should be associated to an event using registerListeners.
749
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
750
+ */
751
+ sendSeekStart(att) {
752
+ if (this.state.goSeekStart()) {
753
+ let ev;
754
+ if (this.isAd()) {
755
+ ev = VideoTracker.Events.AD_SEEK_START;
756
+ } else {
757
+ ev = VideoTracker.Events.CONTENT_SEEK_START;
758
+ }
759
+ // this.send(ev, att);
760
+
761
+ this.isAd()
762
+ ? this.sendVideoAdAction(ev, att)
763
+ : this.sendVideoAction(ev, att);
764
+ }
765
+ }
766
+
767
+ /**
768
+ * Sends associated event and changes view state. An internal state machine will prevent
769
+ * duplicated events. Should be associated to an event using registerListeners.
770
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
771
+ */
772
+ sendSeekEnd(att) {
773
+ if (this.state.goSeekEnd()) {
774
+ att = att || {};
775
+ let ev;
776
+ if (this.isAd()) {
777
+ ev = VideoTracker.Events.AD_SEEK_END;
778
+ att.timeSinceAdSeekBegin = this.state.timeSinceSeekBegin.getDeltaTime();
779
+ } else {
780
+ ev = VideoTracker.Events.CONTENT_SEEK_END;
781
+ att.timeSinceSeekBegin = this.state.timeSinceSeekBegin.getDeltaTime();
782
+ }
783
+ // this.send(ev, att);
784
+
785
+ this.isAd()
786
+ ? this.sendVideoAdAction(ev, att)
787
+ : this.sendVideoAction(ev, att);
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Sends associated event and changes view state. An internal state machine will prevent
793
+ * duplicated events. Should be associated to an event using registerListeners.
794
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
795
+ * @param {String} att.state Download requires a string to distinguish different states.
796
+ */
797
+ sendDownload(att) {
798
+ att = att || {};
799
+ if (!att.state) Log.warn("Called sendDownload without { state: xxxxx }.");
800
+ this.sendVideoAction(VideoTracker.Events.DOWNLOAD, att);
801
+ this.state.goDownload();
802
+ }
803
+
804
+ /**
805
+ * Sends associated event and changes view state. An internal state machine will prevent
806
+ * duplicated events. Should be associated to an event using registerListeners.
807
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
808
+ */
809
+ sendError(att) {
810
+ att = att || {};
811
+
812
+ att.isAd = this.isAd();
813
+ this.state.goError();
814
+ let ev = this.isAd()
815
+ ? VideoTracker.Events.AD_ERROR
816
+ : VideoTracker.Events.CONTENT_ERROR;
817
+ //this.send(ev, att);
818
+
819
+ this.sendVideoErrorAction(ev, att);
820
+ }
821
+
822
+ /**
823
+ * Sends associated event and changes view state. An internal state machine will prevent
824
+ * duplicated events. Should be associated to an event using registerListeners.
825
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
826
+ */
827
+ sendRenditionChanged(att) {
828
+ att = att || {};
829
+ att.timeSinceLastRenditionChange =
830
+ this.state.timeSinceLastRenditionChange.getDeltaTime();
831
+ att.shift = this.getRenditionShift(true);
832
+ let ev;
833
+ if (this.isAd()) {
834
+ ev = VideoTracker.Events.AD_RENDITION_CHANGE;
835
+ } else {
836
+ ev = VideoTracker.Events.CONTENT_RENDITION_CHANGE;
837
+ }
838
+ //this.send(ev, att);
839
+
840
+ this.isAd()
841
+ ? this.sendVideoAdAction(ev, att)
842
+ : this.sendVideoAction(ev, att);
843
+
844
+ this.state.goRenditionChange();
845
+ }
846
+
847
+ /**
848
+ * Sends associated event and changes view state. Heartbeat will automatically be sent every
849
+ * 10 seconds. There's no need to call this manually.
850
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
851
+ * @param {number} att.url Url of the clicked ad.
852
+ *
853
+ */
854
+ sendHeartbeat(att) {
855
+ if (this.state.isRequested) {
856
+ let ev;
857
+
858
+ let elapsedTime = this.getHeartbeat();
859
+ this.state._hb = true;
860
+ elapsedTime = this.adjustElapsedTimeForPause(elapsedTime);
861
+
862
+ if (this.isAd()) {
863
+ ev = VideoTracker.Events.AD_HEARTBEAT;
864
+ if (this.getPlayerName() === "bitmovin-ads") {
865
+ this.sendVideoAdAction(ev, att);
866
+ } else {
867
+ this.sendVideoAdAction(ev, { elapsedTime, ...att });
868
+ }
869
+ } else {
870
+ ev = VideoTracker.Events.CONTENT_HEARTBEAT;
871
+ this.sendVideoAction(ev, { elapsedTime, ...att });
872
+ }
873
+ this.state.goHeartbeat();
874
+ }
875
+ }
876
+
877
+ adjustElapsedTimeForPause(elapsedTime) {
878
+ if (this.state._acc) {
879
+ elapsedTime -= this.state._acc;
880
+ this.state._acc = 0;
881
+ }
882
+
883
+ if (this.state.isPaused) {
884
+ elapsedTime -= this.state.elapsedTime.getDeltaTime();
885
+ if (elapsedTime < 10) elapsedTime = 0;
886
+ this.state.elapsedTime.start();
887
+ }
888
+
889
+ if (this.state._bufferAcc) {
890
+ elapsedTime -= this.state._bufferAcc;
891
+ this.state._bufferAcc = 0;
892
+ } else if (this.state.isBuffering) {
893
+ elapsedTime -= this.state.bufferElapsedTime.getDeltaTime();
894
+ if (elapsedTime < 5) {
895
+ elapsedTime = 0;
896
+ }
897
+ this.state.bufferElapsedTime.start();
898
+ }
899
+
900
+ return Math.max(0, elapsedTime);
901
+ }
902
+
903
+ // Only ads
904
+ /**
905
+ * Sends associated event and changes view state. An internal state machine will prevent
906
+ * duplicated events. Should be associated to an event using registerListeners.
907
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
908
+ */
909
+ sendAdBreakStart(att) {
910
+ if (this.isAd() && this.state.goAdBreakStart()) {
911
+ this.state.totalAdPlaytime = 0;
912
+ if (this.parentTracker) this.parentTracker.state.isPlaying = false;
913
+ // this.send(VideoTracker.Events.AD_BREAK_START, att);
914
+ this.sendVideoAdAction(VideoTracker.Events.AD_BREAK_START, att);
915
+ }
916
+ }
917
+
918
+ /**
919
+ * Sends associated event and changes view state. An internal state machine will prevent
920
+ * duplicated events. Should be associated to an event using registerListeners.
921
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
922
+ */
923
+ sendAdBreakEnd(att) {
924
+ if (this.isAd() && this.state.goAdBreakEnd()) {
925
+ att = att || {};
926
+ att.timeSinceAdBreakBegin =
927
+ this.state.timeSinceAdBreakStart.getDeltaTime();
928
+ //this.send(VideoTracker.Events.AD_BREAK_END, att);
929
+ this.sendVideoAdAction(VideoTracker.Events.AD_BREAK_END, att);
930
+ // Just in case AD_END not arriving, because of an AD_ERROR
931
+ if (this.parentTracker) this.parentTracker.state.isPlaying = true;
932
+ this.stopHeartbeat();
933
+ if (this.parentTracker && this.isAd())
934
+ this.parentTracker.state.goLastAd();
935
+ }
936
+ }
937
+
938
+ /**
939
+ * Sends associated event and changes view state. An internal state machine will prevent
940
+ * duplicated events. Should be associated to an event using registerListeners.
941
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
942
+ * @param {number} att.quartile Number of the quartile.
943
+ */
944
+ sendAdQuartile(att) {
945
+ if (this.isAd()) {
946
+ att = att || {};
947
+ if (!att.quartile)
948
+ Log.warn("Called sendAdQuartile without { quartile: xxxxx }.");
949
+ att.timeSinceLastAdQuartile =
950
+ this.state.timeSinceLastAdQuartile.getDeltaTime();
951
+ //this.send(VideoTracker.Events.AD_QUARTILE, att);
952
+
953
+ this.sendVideoAdAction(VideoTracker.Events.AD_QUARTILE, att);
954
+ this.state.goAdQuartile();
955
+ }
956
+ }
957
+
958
+ /**
959
+ * Sends associated event and changes view state. An internal state machine will prevent
960
+ * duplicated events. Should be associated to an event using registerListeners.
961
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
962
+ * @param {number} att.url Url of the clicked ad.
963
+ */
964
+ sendAdClick(att) {
965
+ if (this.isAd()) {
966
+ att = att || {};
967
+ if (!att.url) Log.warn("Called sendAdClick without { url: xxxxx }.");
968
+ //this.send(VideoTracker.Events.AD_CLICK, att);
969
+ this.sendVideoAdAction(VideoTracker.Events.AD_CLICK, att);
970
+ }
971
+ }
972
+ }
973
+
974
+ /**
975
+ * Enumeration of events fired by this class.
976
+ *
977
+ * @static
978
+ * @memberof VideoTracker
979
+ * @enum {String}
980
+ */
981
+ VideoTracker.Events = {
982
+ // Player
983
+ /** The player is ready to start sending events. */
984
+ PLAYER_READY: "PLAYER_READY",
985
+ /** Downloading data. */
986
+ DOWNLOAD: "DOWNLOAD",
987
+ /** An error happened */
988
+ ERROR: "ERROR",
989
+
990
+ // Video
991
+ /** Content video has been requested. */
992
+ CONTENT_REQUEST: "CONTENT_REQUEST",
993
+ /** Content video started (first frame shown). */
994
+ CONTENT_START: "CONTENT_START",
995
+ /** Content video ended. */
996
+ CONTENT_END: "CONTENT_END",
997
+ /** Content video paused. */
998
+ CONTENT_PAUSE: "CONTENT_PAUSE",
999
+ /** Content video resumed. */
1000
+ CONTENT_RESUME: "CONTENT_RESUME",
1001
+ /** Content video seek started */
1002
+ CONTENT_SEEK_START: "CONTENT_SEEK_START",
1003
+ /** Content video seek ended. */
1004
+ CONTENT_SEEK_END: "CONTENT_SEEK_END",
1005
+ /** Content video beffering started */
1006
+ CONTENT_BUFFER_START: "CONTENT_BUFFER_START",
1007
+ /** Content video buffering ended */
1008
+ CONTENT_BUFFER_END: "CONTENT_BUFFER_END",
1009
+ /** Content video heartbeat, en event that happens once every 30 seconds while the video is playing. */
1010
+ CONTENT_HEARTBEAT: "CONTENT_HEARTBEAT",
1011
+ /** Content video stream qwuality changed. */
1012
+ CONTENT_RENDITION_CHANGE: "CONTENT_RENDITION_CHANGE",
1013
+ /** Content video error. */
1014
+ CONTENT_ERROR: "CONTENT_ERROR",
1015
+
1016
+ // Ads only
1017
+ /** Ad video has been requested. */
1018
+ AD_REQUEST: "AD_REQUEST",
1019
+ /** Ad video started (first frame shown). */
1020
+ AD_START: "AD_START",
1021
+ /** Ad video ended. */
1022
+ AD_END: "AD_END",
1023
+ /** Ad video paused. */
1024
+ AD_PAUSE: "AD_PAUSE",
1025
+ /** Ad video resumed. */
1026
+ AD_RESUME: "AD_RESUME",
1027
+ /** Ad video seek started */
1028
+ AD_SEEK_START: "AD_SEEK_START",
1029
+ /** Ad video seek ended */
1030
+ AD_SEEK_END: "AD_SEEK_END",
1031
+ /** Ad video beffering started */
1032
+ AD_BUFFER_START: "AD_BUFFER_START",
1033
+ /** Ad video beffering ended */
1034
+ AD_BUFFER_END: "AD_BUFFER_END",
1035
+ /** Ad video heartbeat, en event that happens once every 30 seconds while the video is playing. */
1036
+ AD_HEARTBEAT: "AD_HEARTBEAT",
1037
+ /** Ad video stream qwuality changed. */
1038
+ AD_RENDITION_CHANGE: "AD_RENDITION_CHANGE",
1039
+ /** Ad video error. */
1040
+ AD_ERROR: "AD_ERROR",
1041
+ /** Ad break (a block of ads) started. */
1042
+ AD_BREAK_START: "AD_BREAK_START",
1043
+ /** Ad break ended. */
1044
+ AD_BREAK_END: "AD_BREAK_END",
1045
+ /** Ad quartile happened. */
1046
+ AD_QUARTILE: "AD_QUARTILE",
1047
+ /** Ad has been clicked. */
1048
+ AD_CLICK: "AD_CLICK",
1049
+ };
1050
+
1051
+ // Private members
1052
+ function funnelAdEvents(e) {
1053
+ if (e.type === VideoTracker.Events.AD_ERROR) {
1054
+ this.sendVideoErrorAction(e.type, e.data);
1055
+ return;
1056
+ }
1057
+ this.sendVideoAdAction(e.type, e.data);
1058
+ }
1059
+
1060
+ export default VideoTracker;