@newrelic/video-videojs 4.1.1 → 4.2.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.
@@ -12,8 +12,8 @@ if (typeof videojs !== 'undefined') {
12
12
  */
13
13
  registerPlugin('newrelic', function (options) {
14
14
  if (!this.newrelictracker) {
15
- this.newrelictracker = new nrvideo.VideojsTracker(this);
16
- nrvideo.Core.addTracker(this.newrelictracker);
15
+ this.newrelictracker = new nrvideo.VideojsTracker(this, options);
16
+ nrvideo.Core.addTracker(this.newrelictracker, options);
17
17
  }
18
18
  return this.newrelictracker;
19
19
  });
@@ -15,7 +15,7 @@ export default class ContribHlsTech {
15
15
  getRenditionBitrate() {
16
16
  try {
17
17
  var media = this.tech.playlists.media();
18
- if (media && media.attributes) return media.attributes.BANDWIDTH;
18
+ if (media && media.attributes) return Math.round(media.attributes.BANDWIDTH);
19
19
  } catch (err) {}
20
20
  return null;
21
21
  }
@@ -50,7 +50,7 @@ export default class ContribHlsTech {
50
50
  // Return highest available bitrate from all renditions
51
51
  const playlists = this.tech.playlists.master.playlists;
52
52
  if (playlists && playlists.length > 0) {
53
- return Math.max(...playlists.map((p) => p.attributes.BANDWIDTH));
53
+ return Math.round(Math.max(...playlists.map((p) => p.attributes.BANDWIDTH)));
54
54
  }
55
55
  } catch (err) {}
56
56
  return null;
@@ -61,14 +61,14 @@ export default class ContribHlsTech {
61
61
  this.tech.stats?.bandwidth !== undefined &&
62
62
  this.tech.stats.bandwidth > 0
63
63
  ) {
64
- return this.tech.stats.bandwidth;
64
+ return Math.round(this.tech.stats.bandwidth);
65
65
  }
66
66
  return null;
67
67
  }
68
68
 
69
69
  getNetworkDownloadBitrate() {
70
70
  if (this.tech.throughput !== undefined && this.tech.throughput > 0) {
71
- return this.tech.throughput;
71
+ return Math.round(this.tech.throughput);
72
72
  }
73
73
  return null;
74
74
  }
@@ -79,11 +79,10 @@ export default class ContribHlsTech {
79
79
  const media = this.tech.playlists.media();
80
80
  if (media && media.attributes) {
81
81
  // Use AVERAGE-BANDWIDTH if available, fallback to BANDWIDTH
82
- return (
83
- media.attributes['AVERAGE-BANDWIDTH'] ||
82
+ const bitrate = media.attributes['AVERAGE-BANDWIDTH'] ||
84
83
  media.attributes.BANDWIDTH ||
85
- null
86
- );
84
+ null;
85
+ return bitrate !== null ? Math.round(bitrate) : null;
87
86
  }
88
87
  } catch (err) {}
89
88
  return null;
@@ -19,7 +19,7 @@ export default class HlsJs {
19
19
  getRenditionBitrate(tech) {
20
20
  try {
21
21
  var level = this.tech.levels[this.tech.currentLevel];
22
- if (level && level.bitrate) return level.bitrate;
22
+ if (level && level.bitrate) return Math.round(level.bitrate);
23
23
  } catch (err) {}
24
24
  return null;
25
25
  }
@@ -50,7 +50,7 @@ export default class HlsJs {
50
50
  // Get the current active level's bitrate from manifest
51
51
  const level = this.tech.levels[this.tech.currentLevel];
52
52
  if (level && level.bitrate) {
53
- return level.bitrate;
53
+ return Math.round(level.bitrate);
54
54
  }
55
55
  } catch (err) {}
56
56
  return null;
@@ -60,7 +60,7 @@ export default class HlsJs {
60
60
  try {
61
61
  // Return highest available bitrate from all renditions
62
62
  if (this.tech.levels && this.tech.levels.length > 0) {
63
- return Math.max(...this.tech.levels.map(l => l.bitrate));
63
+ return Math.round(Math.max(...this.tech.levels.map(l => l.bitrate)));
64
64
  }
65
65
  } catch (err) {}
66
66
  return null;
@@ -70,14 +70,14 @@ export default class HlsJs {
70
70
  try {
71
71
  // VHS stats.bandwidth
72
72
  if (this.tech.stats && this.tech.stats.bandwidth > 0)
73
- return this.tech.stats.bandwidth;
73
+ return Math.round(this.tech.stats.bandwidth);
74
74
  } catch (err) {}
75
75
  return null;
76
76
  }
77
77
 
78
78
  getNetworkDownloadBitrate() {
79
79
  if (this.tech.throughput && this.tech.throughput > 0) {
80
- return this.tech.throughput;
80
+ return Math.round(this.tech.throughput);
81
81
  }
82
82
  return null;
83
83
  }
@@ -13,7 +13,8 @@ export default class ShakaTech {
13
13
 
14
14
  getRenditionBitrate(tech) {
15
15
  try {
16
- return this.tech.getStats().streamBandwidth;
16
+ const bitrate = this.tech.getStats().streamBandwidth;
17
+ return bitrate ? Math.round(bitrate) : null;
17
18
  } catch (err) {}
18
19
  return null;
19
20
  }
@@ -23,9 +24,9 @@ export default class ShakaTech {
23
24
  // Return highest available bitrate from all variants
24
25
  const tracks = this.tech.getVariantTracks();
25
26
  if (tracks && tracks.length > 0) {
26
- return Math.max(
27
+ return Math.round(Math.max(
27
28
  ...tracks.map((t) => t.videoBandwidth + (t.audioBandwidth || 0)),
28
- );
29
+ ));
29
30
  }
30
31
  } catch (err) {}
31
32
  return null;
@@ -67,7 +68,7 @@ export default class ShakaTech {
67
68
  // Get the current variant's bitrate from manifest (streamBandwidth)
68
69
  var stats = this.tech.getStats();
69
70
  if (stats && stats.streamBandwidth && stats.streamBandwidth > 0) {
70
- return stats.streamBandwidth;
71
+ return Math.round(stats.streamBandwidth);
71
72
  }
72
73
  } catch (err) {}
73
74
  return null;
@@ -78,7 +79,7 @@ export default class ShakaTech {
78
79
  // Use estimatedBandwidth for measured bitrate
79
80
  var stats = this.tech.getStats();
80
81
  if (stats && stats.estimatedBandwidth > 0) {
81
- return stats.estimatedBandwidth;
82
+ return Math.round(stats.estimatedBandwidth);
82
83
  }
83
84
  } catch (err) {}
84
85
  return null;
@@ -89,7 +90,7 @@ export default class ShakaTech {
89
90
  // Shaka: use estimatedBandwidth for download bitrate (no separate property)
90
91
  var stats = this.tech.getStats();
91
92
  if (stats && stats.estimatedBandwidth > 0) {
92
- return stats.estimatedBandwidth;
93
+ return Math.round(stats.estimatedBandwidth);
93
94
  }
94
95
  } catch (err) {}
95
96
  return null;
package/src/tracker.js CHANGED
@@ -8,17 +8,86 @@ import ImaAdsTracker from './ads/ima';
8
8
  import BrightcoveImaAdsTracker from './ads/brightcove-ima';
9
9
  import FreewheelAdsTracker from './ads/freewheel';
10
10
  import DaiAdsTracker from './ads/dai';
11
+ import MediaTailorAdsTracker from './ads/media-tailor';
12
+
13
+ /**
14
+ * Explicit ad tracking modes. Pass via config.ad.type.
15
+ *
16
+ * CSAI (Client-Side Ad Insertion)
17
+ * All CSAI frameworks share the adsready event path. The tracker auto-detects
18
+ * the right framework (Brightcove IMA → IMA → Freewheel → generic) — no sub-type needed.
19
+ *
20
+ * SSAI (Server-Side Ad Insertion) — sub-type is always required.
21
+ * Each SSAI platform has its own SDK and a different activation path.
22
+ * Cannot be auto-detected — declare which one you are using.
23
+ * SSAI.DAI — Google DAI (google.ima.dai.api)
24
+ * SSAI.MT — AWS MediaTailor
25
+ *
26
+ * If config.ad is not set, no ad tracking runs (safe default).
27
+ * To add a new SSAI platform: add a key to SSAI — validation derives from this object.
28
+ */
29
+ export const AD_TRACKING = {
30
+ CSAI: 'csai',
31
+ SSAI: Object.freeze({
32
+ DAI: 'ssai:dai',
33
+ MT: 'ssai:mt',
34
+ }),
35
+ };
11
36
 
12
37
  export default class VideojsTracker extends nrvideo.VideoTracker {
13
38
  constructor(player, options) {
14
39
  super(player, options);
15
-
40
+ this.options = options;
16
41
  this.isContentEnd = false;
17
42
  this.imaAdCuePoints = '';
18
43
  this.daiInitialized = false;
44
+ this.adTracking = options?.config?.ad?.type || null;
45
+
46
+ // When options are provided but config.ad is not declared, no ad tracking
47
+ // will run. Warn so the caller knows to set it explicitly.
48
+ if (options && !this.adTracking) {
49
+ nrvideo.Log.warn(
50
+ 'VideojsTracker: config.ad not set — no ad tracking will run. ' +
51
+ 'Set config.ad.type to enable it (e.g. AD_TRACKING.CSAI, AD_TRACKING.SSAI.MT).'
52
+ );
53
+ }
54
+
55
+ // Merge config.ad options (segmentPrefix, trackingUrl) into the internal
56
+ // mediatailor object that MediaTailorAdsTracker reads.
57
+ if (this.adTracking === AD_TRACKING.SSAI.MT) {
58
+ const adConfig = options?.config?.ad || {};
59
+ this.options = Object.assign({}, this.options, {
60
+ mediatailor: {
61
+ segmentPrefix: adConfig.segmentPrefix,
62
+ trackingUrl: adConfig.trackingUrl,
63
+ ...this.options?.mediatailor,
64
+ },
65
+ });
66
+ }
67
+
19
68
  nrvideo.Core.addTracker(this, options);
20
69
  }
21
70
 
71
+ /**
72
+ * Sets the ad tracking mode at runtime.
73
+ * Must be called before the first loadstart / adsready event to take effect.
74
+ * @param {'csai'|'ssai:dai'|'ssai:mt'} type
75
+ */
76
+ setAdTracking(type) {
77
+ const valid = [
78
+ AD_TRACKING.CSAI,
79
+ ...Object.values(AD_TRACKING.SSAI),
80
+ ];
81
+ if (!valid.includes(type)) {
82
+ nrvideo.Log.warn(
83
+ `VideojsTracker.setAdTracking: unknown value "${type}". Valid values: ${valid.join(', ')}`
84
+ );
85
+ return;
86
+ }
87
+ this.adTracking = type;
88
+ nrvideo.Log.debug(`VideojsTracker: adTracking set to "${type}"`);
89
+ }
90
+
22
91
  getTech() {
23
92
  let tech = this.player.tech({ IWillNotUseThisInPlugins: true });
24
93
 
@@ -125,17 +194,18 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
125
194
  if (tech?.vhs?.playlists?.media()) {
126
195
  const activePlaylist = tech.vhs.playlists.media();
127
196
  // Use AVERAGE-BANDWIDTH if available, fallback to BANDWIDTH
128
- return (
197
+ const bitrate =
129
198
  activePlaylist.attributes['AVERAGE-BANDWIDTH'] ||
130
199
  activePlaylist.attributes.BANDWIDTH ||
131
- null
132
- );
200
+ null;
201
+ return bitrate !== null ? Math.round(bitrate) : null;
133
202
  }
134
203
 
135
204
  // 2. Fallback to tech wrappers (Shaka/Hls.js) if they have a getBitrate method
136
205
  const techWrapper = this.getTech();
137
206
  if (techWrapper?.getBitrate) {
138
- return techWrapper.getBitrate();
207
+ const bitrate = techWrapper.getBitrate();
208
+ return bitrate !== null ? Math.round(bitrate) : null;
139
209
  }
140
210
  } catch (err) {
141
211
  /* ignore */
@@ -161,13 +231,14 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
161
231
  const maxBitrate = Math.max(
162
232
  ...allRenditions.map((p) => p.attributes.BANDWIDTH || 0),
163
233
  );
164
- return maxBitrate > 0 ? maxBitrate : null;
234
+ return maxBitrate > 0 ? Math.round(maxBitrate) : null;
165
235
  }
166
236
 
167
237
  // Fallback to tech wrappers (Shaka/Hls.js)
168
238
  const techWrapper = this.getTech();
169
239
  if (techWrapper?.getManifestBitrate) {
170
- return techWrapper.getManifestBitrate();
240
+ const bitrate = techWrapper.getManifestBitrate();
241
+ return bitrate !== null ? Math.round(bitrate) : null;
171
242
  }
172
243
  } catch (e) {
173
244
  /* ignore */
@@ -181,13 +252,14 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
181
252
 
182
253
  // VHS stats.bandwidth
183
254
  if (tech?.vhs?.stats?.bandwidth && tech.vhs.stats.bandwidth > 0) {
184
- return tech.vhs.stats.bandwidth;
255
+ return Math.round(tech.vhs.stats.bandwidth);
185
256
  }
186
257
 
187
258
  // Fallback to tech wrappers (Shaka/Hls.js)
188
259
  const techWrapper = this.getTech();
189
260
  if (techWrapper?.getSegmentDownloadBitrate) {
190
- return techWrapper.getSegmentDownloadBitrate();
261
+ const bitrate = techWrapper.getSegmentDownloadBitrate();
262
+ return bitrate !== null ? Math.round(bitrate) : null;
191
263
  }
192
264
  } catch (err) {
193
265
  /* ignore */
@@ -199,13 +271,14 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
199
271
  const tech = this.player.tech({ IWillNotUseThisInPlugins: true });
200
272
 
201
273
  if (tech?.vhs?.throughput && tech.vhs.throughput > 0) {
202
- return tech.vhs.throughput;
274
+ return Math.round(tech.vhs.throughput);
203
275
  }
204
276
 
205
277
  // Fallback to tech wrapper implementation
206
278
  const techWrapper = this.getTech();
207
279
  if (techWrapper?.getNetworkDownloadBitrate) {
208
- return techWrapper.getNetworkDownloadBitrate();
280
+ const bitrate = techWrapper.getNetworkDownloadBitrate();
281
+ return bitrate !== null ? Math.round(bitrate) : null;
209
282
  }
210
283
 
211
284
  return null;
@@ -272,6 +345,7 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
272
345
  this.onTimeupdate = this.onTimeupdate.bind(this);
273
346
  this.OnAdsAllpodsCompleted = this.OnAdsAllpodsCompleted.bind(this);
274
347
  this.onStreamManager = this.onStreamManager.bind(this);
348
+ this.onCanPlayThrough = this.onCanPlayThrough.bind(this);
275
349
 
276
350
  this.player.on('loadstart', this.onDownload);
277
351
  this.player.on('loadeddata', this.onDownload);
@@ -292,6 +366,7 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
292
366
  this.player.on('timeupdate', this.onTimeupdate);
293
367
  this.player.on('ads-allpods-completed', this.OnAdsAllpodsCompleted);
294
368
  this.player.on('stream-manager', this.onStreamManager);
369
+ this.player.on('canplaythrough', this.onCanPlayThrough);
295
370
  }
296
371
 
297
372
  unregisterListeners() {
@@ -314,15 +389,31 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
314
389
  this.player.off('timeupdate', this.onTimeupdate);
315
390
  this.player.off('ads-allpods-completed', this.OnAdsAllpodsCompleted);
316
391
  this.player.off('stream-manager', this.onStreamManager);
392
+ this.player.off('canplaythrough', this.onCanPlayThrough);
317
393
  }
318
394
 
319
395
  onDownload(e) {
320
396
  this.sendDownload({ state: e.type });
397
+
398
+ if (
399
+ this.adTracking === AD_TRACKING.SSAI.MT &&
400
+ !this.adsTracker &&
401
+ e.type === 'loadstart'
402
+ ) {
403
+ nrvideo.Log.debug('VideojsTracker: Creating MediaTailorAdsTracker');
404
+ this.setAdsTracker(new MediaTailorAdsTracker(this.player, this.options));
405
+ // MediaTailor SSAI starts with content, not ads (unlike client-side frameworks)
406
+ this.adsTracker.setIsAd(false);
407
+ }
321
408
  }
322
409
 
323
410
  // DAI methods
324
411
  onStreamManager(event) {
325
- if (!this.adsTracker && event.StreamManager) {
412
+ if (
413
+ this.adTracking === AD_TRACKING.SSAI.DAI &&
414
+ !this.adsTracker &&
415
+ event.StreamManager
416
+ ) {
326
417
  const daiTracker = new DaiAdsTracker(this.player);
327
418
  daiTracker.setStreamManager(event.StreamManager);
328
419
  this.setAdsTracker(daiTracker);
@@ -331,20 +422,29 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
331
422
  // DAI methods end
332
423
 
333
424
  onAdsready() {
425
+ // SSAI platforms never fire adsready — skip when an SSAI type is explicitly set.
426
+ if (Object.values(AD_TRACKING.SSAI).includes(this.adTracking)) return;
427
+
428
+ // config.ad.type is not set — warn and fall back to auto-detection.
429
+ if (!this.adTracking) {
430
+ nrvideo.Log.warn(
431
+ 'VideojsTracker: adsready fired but config.ad.type is not set — ' +
432
+ 'attempting CSAI auto-detection for backward compatibility.'
433
+ );
434
+ }
435
+
334
436
  if (!this.adsTracker) {
335
437
  if (BrightcoveImaAdsTracker.isUsing(this.player)) {
336
- // BC IMA
438
+ nrvideo.Log.debug('VideojsTracker: auto-detected BrightcoveImaAdsTracker');
337
439
  this.setAdsTracker(new BrightcoveImaAdsTracker(this.player));
338
440
  } else if (ImaAdsTracker.isUsing(this.player)) {
339
- // IMA
441
+ nrvideo.Log.debug('VideojsTracker: auto-detected ImaAdsTracker');
340
442
  this.setAdsTracker(new ImaAdsTracker(this.player));
341
443
  } else if (FreewheelAdsTracker.isUsing(this.player)) {
342
- // FW
343
-
444
+ nrvideo.Log.debug('VideojsTracker: auto-detected FreewheelAdsTracker');
344
445
  this.setAdsTracker(new FreewheelAdsTracker(this.player));
345
- // } else if (OnceAdsTracker.isUsing(this)) { // Once
346
446
  } else {
347
- // Generic
447
+ nrvideo.Log.debug('VideojsTracker: no specific CSAI framework detected, using generic VideojsAdsTracker');
348
448
  this.setAdsTracker(new VideojsAdsTracker(this.player));
349
449
  }
350
450
  }
@@ -369,15 +469,50 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
369
469
  this.FreewheelAdsCompleted = true;
370
470
  }
371
471
 
472
+ /**
473
+ * Check if ads tracker is currently in ad mode
474
+ * @returns {boolean} True if ads are playing
475
+ */
476
+ isAdsTrackerActive() {
477
+ // For CSAI: videojs-contrib-ads manages ad state exclusively
478
+ if (this.player.ads) {
479
+ return this.player.ads.state === 'ads-playback';
480
+ }
481
+ // For SSAI (no player.ads): check tracker's isAd flag (manually toggled)
482
+ return this.adsTracker && this.adsTracker.isAd && this.adsTracker.isAd();
483
+ }
484
+
372
485
  onPlay() {
373
486
  this.sendRequest();
374
487
  }
375
488
 
376
489
  onPause() {
490
+ // Don't send CONTENT_PAUSE if ads are playing (ads tracker handles it)
491
+ if (this.isAdsTrackerActive()) {
492
+ return;
493
+ }
494
+ // Don't send CONTENT_PAUSE if video has ended (CONTENT_END will be sent instead)
495
+ if (this.player.ended()) {
496
+ return;
497
+ }
377
498
  this.sendPause();
378
499
  }
379
500
 
501
+ onCanPlayThrough() {
502
+ // Don't send CONTENT_BUFFER_END if ads are playing (ads tracker handles it)
503
+ if (this.isAdsTrackerActive()) {
504
+ return;
505
+ }
506
+ // Send BUFFER_END when buffer fills, even if paused
507
+ // This ensures accurate buffer duration metrics when user pauses during buffering
508
+ this.sendBufferEnd();
509
+ }
510
+
380
511
  onPlaying() {
512
+ // Don't send CONTENT_RESUME if ads are playing (ads tracker handles it)
513
+ if (this.isAdsTrackerActive()) {
514
+ return;
515
+ }
381
516
  this.sendResume();
382
517
  this.sendBufferEnd();
383
518
  }
@@ -402,10 +537,18 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
402
537
  }
403
538
 
404
539
  onSeeking() {
540
+ // Don't send CONTENT_SEEK_START if ads are playing (ads tracker handles it)
541
+ if (this.isAdsTrackerActive()) {
542
+ return;
543
+ }
405
544
  this.sendSeekStart();
406
545
  }
407
546
 
408
547
  onSeeked() {
548
+ // Don't send CONTENT_SEEK_END if ads are playing (ads tracker handles it)
549
+ if (this.isAdsTrackerActive()) {
550
+ return;
551
+ }
409
552
  this.sendSeekEnd();
410
553
  }
411
554
 
@@ -420,6 +563,10 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
420
563
  }
421
564
 
422
565
  onWaiting(e) {
566
+ // Don't send CONTENT_BUFFER_START if ads are playing (ads tracker handles it)
567
+ if (this.isAdsTrackerActive()) {
568
+ return;
569
+ }
423
570
  this.sendBufferStart();
424
571
  }
425
572
 
@@ -430,6 +577,11 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
430
577
  }
431
578
  }
432
579
 
580
+ // Attach AD_TRACKING and Log as static properties so UMD callers can use
581
+ // VideojsTracker.AD_TRACKING and VideojsTracker.Log without importing named exports.
582
+ VideojsTracker.AD_TRACKING = AD_TRACKING;
583
+ VideojsTracker.Log = nrvideo.Log;
584
+
433
585
  // Static members
434
586
  export {
435
587
  HlsJsTech,
@@ -439,4 +591,6 @@ export {
439
591
  ImaAdsTracker,
440
592
  BrightcoveImaAdsTracker,
441
593
  FreewheelAdsTracker,
594
+ DaiAdsTracker,
595
+ MediaTailorAdsTracker,
442
596
  };