@newrelic/video-videojs 4.1.2 → 4.2.1
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.
- package/CHANGELOG.md +62 -1
- package/README.md +170 -40
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.LICENSE.txt +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.LICENSE.txt +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/umd/newrelic-video-videojs.min.js +1 -1
- package/dist/umd/newrelic-video-videojs.min.js.LICENSE.txt +1 -1
- package/dist/umd/newrelic-video-videojs.min.js.map +1 -1
- package/package.json +1 -1
- package/src/ads/media-tailor.js +126 -91
- package/src/ads/utils/mt-constants.js +2 -1
- package/src/ads/utils/mt.js +44 -22
- package/src/register-plugin.js +2 -2
- package/src/tracker.js +150 -16
|
@@ -36,7 +36,8 @@ export const DASH_SCTE35_EVENT_STREAM_SELECTOR =
|
|
|
36
36
|
export const HLS_MIME_TYPE = 'application/vnd.apple.mpegurl'; // MIME type used to detect Safari/native HLS playback support
|
|
37
37
|
|
|
38
38
|
// MediaTailor URL Patterns
|
|
39
|
-
export const MT_SEGMENT_PATTERN = 'segments.mediatailor'; // Identifies MediaTailor ad segments
|
|
39
|
+
export const MT_SEGMENT_PATTERN = 'segments.mediatailor'; // Identifies MediaTailor ad segments (AWS default hostname)
|
|
40
|
+
export const MT_DEFAULT_AD_SEGMENT_PATH = '/tm/'; // Default ad-segment path per AWS CDN integration guide (custom-CDN setups)
|
|
40
41
|
|
|
41
42
|
export const TRACKING_API_TIMEOUT_MS = 5000; // Keep tracking metadata requests within the guide's 5s end-to-end budget
|
|
42
43
|
export const DEFAULT_LIVE_POLL_INTERVAL_MS = 5000; // Temporary fallback until manifest metadata provides cadence
|
package/src/ads/utils/mt.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* Helper functions for AWS MediaTailor ad tracking
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import nrvideo from '@newrelic/video-core';
|
|
7
|
+
|
|
8
|
+
const nrvideoCore = nrvideo.default || nrvideo;
|
|
9
|
+
const Log = nrvideoCore.Log;
|
|
10
|
+
|
|
6
11
|
import {
|
|
7
12
|
DASH_MANIFEST_EXTENSION,
|
|
8
13
|
DASH_SCTE35_EVENT_STREAM_SELECTOR,
|
|
@@ -21,6 +26,7 @@ import {
|
|
|
21
26
|
MT_HLS_CUE_IN_TAG,
|
|
22
27
|
MT_HLS_CUE_OUT_TAG,
|
|
23
28
|
MT_SEGMENT_PATTERN,
|
|
29
|
+
MT_DEFAULT_AD_SEGMENT_PATH,
|
|
24
30
|
MIN_AD_DURATION,
|
|
25
31
|
AD_TIMING_TOLERANCE,
|
|
26
32
|
SCTE35_SCHEME_MARKER,
|
|
@@ -84,16 +90,30 @@ export function buildTrackingEndpointUrl(manifestUrl) {
|
|
|
84
90
|
/**
|
|
85
91
|
* Checks if segment is a MediaTailor ad segment
|
|
86
92
|
*/
|
|
87
|
-
export function isMediaTailorSegment(segment) {
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
export function isMediaTailorSegment(segment, { adSegmentPrefix } = {}) {
|
|
94
|
+
const candidates = [MT_SEGMENT_PATTERN, MT_DEFAULT_AD_SEGMENT_PATH];
|
|
95
|
+
if (adSegmentPrefix) candidates.push(adSegmentPrefix);
|
|
96
|
+
|
|
97
|
+
const mapUri = (segment.map && segment.map.uri) || '';
|
|
98
|
+
const segUri = segment.uri || '';
|
|
99
|
+
|
|
100
|
+
return candidates.some(
|
|
101
|
+
(marker) => mapUri.includes(marker) || segUri.includes(marker),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function whichAdSegmentMarker(segment, { adSegmentPrefix } = {}) {
|
|
106
|
+
const labeled = [
|
|
107
|
+
{ marker: MT_SEGMENT_PATTERN, label: 'aws-hostname (segments.mediatailor)' },
|
|
108
|
+
{ marker: MT_DEFAULT_AD_SEGMENT_PATH, label: 'default-cdn-path (/tm/)' },
|
|
109
|
+
];
|
|
110
|
+
if (adSegmentPrefix) labeled.push({ marker: adSegmentPrefix, label: `custom-prefix (${adSegmentPrefix})` });
|
|
111
|
+
|
|
112
|
+
const mapUri = (segment.map && segment.map.uri) || '';
|
|
113
|
+
const segUri = segment.uri || '';
|
|
114
|
+
|
|
115
|
+
const hit = labeled.find(({ marker }) => mapUri.includes(marker) || segUri.includes(marker));
|
|
116
|
+
return hit ? hit.label : null;
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
/**
|
|
@@ -350,7 +370,7 @@ export function parseHlsManifestForAdBreaks(manifestText) {
|
|
|
350
370
|
/**
|
|
351
371
|
* Detects ads from VHS playlist using discontinuityStarts and MediaTailor segments
|
|
352
372
|
*/
|
|
353
|
-
export function detectAdBreaksFromVhsPlaylist(playlist) {
|
|
373
|
+
export function detectAdBreaksFromVhsPlaylist(playlist, { adSegmentPrefix } = {}) {
|
|
354
374
|
const segments = playlist.segments;
|
|
355
375
|
const discontinuityStarts = playlist.discontinuityStarts || [];
|
|
356
376
|
const adBreaks = [];
|
|
@@ -360,7 +380,7 @@ export function detectAdBreaksFromVhsPlaylist(playlist) {
|
|
|
360
380
|
let currentTime = 0;
|
|
361
381
|
|
|
362
382
|
segments.forEach((segment, index) => {
|
|
363
|
-
const isMTSegment = isMediaTailorSegment(segment);
|
|
383
|
+
const isMTSegment = isMediaTailorSegment(segment, { adSegmentPrefix });
|
|
364
384
|
const hasDiscontinuity = discontinuityStarts.includes(index);
|
|
365
385
|
|
|
366
386
|
if (isMTSegment) {
|
|
@@ -627,19 +647,19 @@ export function parseIsoDuration(durationStr) {
|
|
|
627
647
|
* SINGLE_PERIOD: the entire stream is one period; ads are signalled via
|
|
628
648
|
* SCTE-35 <EventStream> elements inside that period.
|
|
629
649
|
*/
|
|
630
|
-
export function parseDashManifestForAdBreaks(xmlText) {
|
|
650
|
+
export function parseDashManifestForAdBreaks(xmlText, { adSegmentPrefix } = {}) {
|
|
631
651
|
const parser = new DOMParser();
|
|
632
652
|
const xml = parser.parseFromString(xmlText, 'text/xml');
|
|
633
653
|
const ads = [];
|
|
634
654
|
|
|
635
655
|
const parserError = xml.querySelector('parsererror');
|
|
636
656
|
if (parserError) {
|
|
637
|
-
|
|
657
|
+
Log.error('[MT] DASH XML parse error:', parserError.textContent);
|
|
638
658
|
return ads;
|
|
639
659
|
}
|
|
640
660
|
|
|
641
661
|
const periods = xml.querySelectorAll('Period');
|
|
642
|
-
|
|
662
|
+
Log.debug(`[MT] Found ${periods.length} Period(s) in DASH manifest`);
|
|
643
663
|
|
|
644
664
|
if (periods.length > 1) {
|
|
645
665
|
// ── MULTI_PERIOD ──────────────────────────────────────────────────────────
|
|
@@ -648,7 +668,9 @@ export function parseDashManifestForAdBreaks(xmlText) {
|
|
|
648
668
|
const baseUrlEl = period.querySelector('BaseURL');
|
|
649
669
|
const baseUrl = baseUrlEl ? baseUrlEl.textContent.trim() : '';
|
|
650
670
|
|
|
651
|
-
|
|
671
|
+
const adCandidates = [MT_SEGMENT_PATTERN, MT_DEFAULT_AD_SEGMENT_PATH];
|
|
672
|
+
if (adSegmentPrefix) adCandidates.push(adSegmentPrefix);
|
|
673
|
+
if (!adCandidates.some((m) => baseUrl.includes(m))) {
|
|
652
674
|
return; // content period
|
|
653
675
|
}
|
|
654
676
|
|
|
@@ -657,11 +679,11 @@ export function parseDashManifestForAdBreaks(xmlText) {
|
|
|
657
679
|
const duration = parseIsoDuration(period.getAttribute('duration') || '');
|
|
658
680
|
|
|
659
681
|
if (duration < MIN_AD_DURATION) {
|
|
660
|
-
|
|
682
|
+
Log.debug(`[MT] Skipping period ${periodId} - duration too short (${duration}s)`);
|
|
661
683
|
return;
|
|
662
684
|
}
|
|
663
685
|
|
|
664
|
-
|
|
686
|
+
Log.debug(`[MT] Ad period detected: ${periodId}`, { startTime, duration });
|
|
665
687
|
|
|
666
688
|
ads.push({
|
|
667
689
|
id: periodId,
|
|
@@ -686,7 +708,7 @@ export function parseDashManifestForAdBreaks(xmlText) {
|
|
|
686
708
|
DASH_SCTE35_EVENT_STREAM_SELECTOR,
|
|
687
709
|
);
|
|
688
710
|
|
|
689
|
-
|
|
711
|
+
Log.debug(`[MT] Found ${eventStreams.length} SCTE-35 EventStream(s) in single-period manifest`);
|
|
690
712
|
|
|
691
713
|
eventStreams.forEach((stream) => {
|
|
692
714
|
const timescale = parseFloat(stream.getAttribute('timescale') || '1');
|
|
@@ -702,11 +724,11 @@ export function parseDashManifestForAdBreaks(xmlText) {
|
|
|
702
724
|
const durationSeconds = timescale !== 1 ? duration / timescale : duration;
|
|
703
725
|
|
|
704
726
|
if (durationSeconds < MIN_AD_DURATION) {
|
|
705
|
-
|
|
727
|
+
Log.debug(`[MT] Skipping event ${eventId} - duration too short (${durationSeconds}s)`);
|
|
706
728
|
return;
|
|
707
729
|
}
|
|
708
730
|
|
|
709
|
-
|
|
731
|
+
Log.debug(`[MT] SCTE-35 event detected: ${eventId}`, { startTime, durationSeconds });
|
|
710
732
|
|
|
711
733
|
ads.push({
|
|
712
734
|
id: eventId,
|
|
@@ -727,7 +749,7 @@ export function parseDashManifestForAdBreaks(xmlText) {
|
|
|
727
749
|
});
|
|
728
750
|
}
|
|
729
751
|
|
|
730
|
-
|
|
752
|
+
Log.debug(`[MT] Parsed ${ads.length} valid ad break(s) from DASH manifest`);
|
|
731
753
|
return ads;
|
|
732
754
|
}
|
|
733
755
|
|
package/src/register-plugin.js
CHANGED
|
@@ -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
|
});
|
package/src/tracker.js
CHANGED
|
@@ -10,6 +10,30 @@ import FreewheelAdsTracker from './ads/freewheel';
|
|
|
10
10
|
import DaiAdsTracker from './ads/dai';
|
|
11
11
|
import MediaTailorAdsTracker from './ads/media-tailor';
|
|
12
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
|
+
};
|
|
36
|
+
|
|
13
37
|
export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
14
38
|
constructor(player, options) {
|
|
15
39
|
super(player, options);
|
|
@@ -17,9 +41,51 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
17
41
|
this.isContentEnd = false;
|
|
18
42
|
this.imaAdCuePoints = '';
|
|
19
43
|
this.daiInitialized = false;
|
|
44
|
+
this.adTracking = options?.config?.ad?.type || null;
|
|
45
|
+
this.previousBitrate = null;
|
|
46
|
+
|
|
47
|
+
// When options are provided but config.ad is not declared, no ad tracking
|
|
48
|
+
// will run. Warn so the caller knows to set it explicitly.
|
|
49
|
+
if (options && !this.adTracking) {
|
|
50
|
+
nrvideo.Log.warn(
|
|
51
|
+
'VideojsTracker: config.ad not set — no ad tracking will run. ' +
|
|
52
|
+
'Set config.ad.type to enable it (e.g. AD_TRACKING.CSAI, AD_TRACKING.SSAI.MT).',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Merge config.ad options (segmentPrefix, trackingUrl) into the internal
|
|
57
|
+
// mediatailor object that MediaTailorAdsTracker reads.
|
|
58
|
+
if (this.adTracking === AD_TRACKING.SSAI.MT) {
|
|
59
|
+
const adConfig = options?.config?.ad || {};
|
|
60
|
+
this.options = Object.assign({}, this.options, {
|
|
61
|
+
mediatailor: {
|
|
62
|
+
segmentPrefix: adConfig.segmentPrefix,
|
|
63
|
+
trackingUrl: adConfig.trackingUrl,
|
|
64
|
+
...this.options?.mediatailor,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
20
69
|
nrvideo.Core.addTracker(this, options);
|
|
21
70
|
}
|
|
22
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Sets the ad tracking mode at runtime.
|
|
74
|
+
* Must be called before the first loadstart / adsready event to take effect.
|
|
75
|
+
* @param {'csai'|'ssai:dai'|'ssai:mt'} type
|
|
76
|
+
*/
|
|
77
|
+
setAdTracking(type) {
|
|
78
|
+
const valid = [AD_TRACKING.CSAI, ...Object.values(AD_TRACKING.SSAI)];
|
|
79
|
+
if (!valid.includes(type)) {
|
|
80
|
+
nrvideo.Log.warn(
|
|
81
|
+
`VideojsTracker.setAdTracking: unknown value "${type}". Valid values: ${valid.join(', ')}`,
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.adTracking = type;
|
|
86
|
+
nrvideo.Log.debug(`VideojsTracker: adTracking set to "${type}"`);
|
|
87
|
+
}
|
|
88
|
+
|
|
23
89
|
getTech() {
|
|
24
90
|
let tech = this.player.tech({ IWillNotUseThisInPlugins: true });
|
|
25
91
|
|
|
@@ -126,7 +192,8 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
126
192
|
if (tech?.vhs?.playlists?.media()) {
|
|
127
193
|
const activePlaylist = tech.vhs.playlists.media();
|
|
128
194
|
// Use AVERAGE-BANDWIDTH if available, fallback to BANDWIDTH
|
|
129
|
-
const bitrate =
|
|
195
|
+
const bitrate =
|
|
196
|
+
activePlaylist.attributes['AVERAGE-BANDWIDTH'] ||
|
|
130
197
|
activePlaylist.attributes.BANDWIDTH ||
|
|
131
198
|
null;
|
|
132
199
|
return bitrate !== null ? Math.round(bitrate) : null;
|
|
@@ -276,6 +343,7 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
276
343
|
this.onTimeupdate = this.onTimeupdate.bind(this);
|
|
277
344
|
this.OnAdsAllpodsCompleted = this.OnAdsAllpodsCompleted.bind(this);
|
|
278
345
|
this.onStreamManager = this.onStreamManager.bind(this);
|
|
346
|
+
this.onCanPlayThrough = this.onCanPlayThrough.bind(this);
|
|
279
347
|
|
|
280
348
|
this.player.on('loadstart', this.onDownload);
|
|
281
349
|
this.player.on('loadeddata', this.onDownload);
|
|
@@ -296,6 +364,16 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
296
364
|
this.player.on('timeupdate', this.onTimeupdate);
|
|
297
365
|
this.player.on('ads-allpods-completed', this.OnAdsAllpodsCompleted);
|
|
298
366
|
this.player.on('stream-manager', this.onStreamManager);
|
|
367
|
+
this.player.on('canplaythrough', this.onCanPlayThrough);
|
|
368
|
+
|
|
369
|
+
this.onQualityLevelChange = this.onQualityLevelChange.bind(this);
|
|
370
|
+
// Get qualityLevels lazily after player is ready
|
|
371
|
+
if (typeof this.player.qualityLevels === 'function') {
|
|
372
|
+
this.qualityLevels = this.player.qualityLevels();
|
|
373
|
+
if (this.qualityLevels) {
|
|
374
|
+
this.qualityLevels.on('change', this.onQualityLevelChange);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
299
377
|
}
|
|
300
378
|
|
|
301
379
|
unregisterListeners() {
|
|
@@ -318,30 +396,34 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
318
396
|
this.player.off('timeupdate', this.onTimeupdate);
|
|
319
397
|
this.player.off('ads-allpods-completed', this.OnAdsAllpodsCompleted);
|
|
320
398
|
this.player.off('stream-manager', this.onStreamManager);
|
|
399
|
+
this.player.off('canplaythrough', this.onCanPlayThrough);
|
|
400
|
+
if (this.qualityLevels) {
|
|
401
|
+
this.qualityLevels.off('change', this.onQualityLevelChange);
|
|
402
|
+
}
|
|
321
403
|
}
|
|
322
404
|
|
|
323
405
|
onDownload(e) {
|
|
324
406
|
this.sendDownload({ state: e.type });
|
|
325
407
|
|
|
326
|
-
// Check if MediaTailor should be used after the source is loaded
|
|
327
|
-
// Only check on 'loadstart' to avoid multiple checks
|
|
328
408
|
if (
|
|
409
|
+
this.adTracking === AD_TRACKING.SSAI.MT &&
|
|
329
410
|
!this.adsTracker &&
|
|
330
|
-
e.type === 'loadstart'
|
|
331
|
-
MediaTailorAdsTracker.isUsing(this.player)
|
|
411
|
+
e.type === 'loadstart'
|
|
332
412
|
) {
|
|
333
|
-
|
|
334
|
-
'VideojsTracker: Creating MediaTailorAdsTracker after source load'
|
|
335
|
-
);
|
|
413
|
+
nrvideo.Log.debug('VideojsTracker: Creating MediaTailorAdsTracker');
|
|
336
414
|
this.setAdsTracker(new MediaTailorAdsTracker(this.player, this.options));
|
|
337
|
-
// MediaTailor SSAI starts with content, not ads (unlike client-side
|
|
415
|
+
// MediaTailor SSAI starts with content, not ads (unlike client-side frameworks)
|
|
338
416
|
this.adsTracker.setIsAd(false);
|
|
339
417
|
}
|
|
340
418
|
}
|
|
341
419
|
|
|
342
420
|
// DAI methods
|
|
343
421
|
onStreamManager(event) {
|
|
344
|
-
if (
|
|
422
|
+
if (
|
|
423
|
+
this.adTracking === AD_TRACKING.SSAI.DAI &&
|
|
424
|
+
!this.adsTracker &&
|
|
425
|
+
event.StreamManager
|
|
426
|
+
) {
|
|
345
427
|
const daiTracker = new DaiAdsTracker(this.player);
|
|
346
428
|
daiTracker.setStreamManager(event.StreamManager);
|
|
347
429
|
this.setAdsTracker(daiTracker);
|
|
@@ -350,20 +432,33 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
350
432
|
// DAI methods end
|
|
351
433
|
|
|
352
434
|
onAdsready() {
|
|
435
|
+
// SSAI platforms never fire adsready — skip when an SSAI type is explicitly set.
|
|
436
|
+
if (Object.values(AD_TRACKING.SSAI).includes(this.adTracking)) return;
|
|
437
|
+
|
|
438
|
+
// config.ad.type is not set — warn and fall back to auto-detection.
|
|
439
|
+
if (!this.adTracking) {
|
|
440
|
+
nrvideo.Log.warn(
|
|
441
|
+
'VideojsTracker: adsready fired but config.ad.type is not set — ' +
|
|
442
|
+
'attempting CSAI auto-detection for backward compatibility.',
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
353
446
|
if (!this.adsTracker) {
|
|
354
447
|
if (BrightcoveImaAdsTracker.isUsing(this.player)) {
|
|
355
|
-
|
|
448
|
+
nrvideo.Log.debug(
|
|
449
|
+
'VideojsTracker: auto-detected BrightcoveImaAdsTracker',
|
|
450
|
+
);
|
|
356
451
|
this.setAdsTracker(new BrightcoveImaAdsTracker(this.player));
|
|
357
452
|
} else if (ImaAdsTracker.isUsing(this.player)) {
|
|
358
|
-
|
|
453
|
+
nrvideo.Log.debug('VideojsTracker: auto-detected ImaAdsTracker');
|
|
359
454
|
this.setAdsTracker(new ImaAdsTracker(this.player));
|
|
360
455
|
} else if (FreewheelAdsTracker.isUsing(this.player)) {
|
|
361
|
-
|
|
362
|
-
|
|
456
|
+
nrvideo.Log.debug('VideojsTracker: auto-detected FreewheelAdsTracker');
|
|
363
457
|
this.setAdsTracker(new FreewheelAdsTracker(this.player));
|
|
364
|
-
// } else if (OnceAdsTracker.isUsing(this)) { // Once
|
|
365
458
|
} else {
|
|
366
|
-
|
|
459
|
+
nrvideo.Log.debug(
|
|
460
|
+
'VideojsTracker: no specific CSAI framework detected, using generic VideojsAdsTracker',
|
|
461
|
+
);
|
|
367
462
|
this.setAdsTracker(new VideojsAdsTracker(this.player));
|
|
368
463
|
}
|
|
369
464
|
}
|
|
@@ -393,6 +488,11 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
393
488
|
* @returns {boolean} True if ads are playing
|
|
394
489
|
*/
|
|
395
490
|
isAdsTrackerActive() {
|
|
491
|
+
// For CSAI: videojs-contrib-ads manages ad state exclusively
|
|
492
|
+
if (this.player.ads) {
|
|
493
|
+
return this.player.ads.state === 'ads-playback';
|
|
494
|
+
}
|
|
495
|
+
// For SSAI (no player.ads): check tracker's isAd flag (manually toggled)
|
|
396
496
|
return this.adsTracker && this.adsTracker.isAd && this.adsTracker.isAd();
|
|
397
497
|
}
|
|
398
498
|
|
|
@@ -412,6 +512,16 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
412
512
|
this.sendPause();
|
|
413
513
|
}
|
|
414
514
|
|
|
515
|
+
onCanPlayThrough() {
|
|
516
|
+
// Don't send CONTENT_BUFFER_END if ads are playing (ads tracker handles it)
|
|
517
|
+
if (this.isAdsTrackerActive()) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
// Send BUFFER_END when buffer fills, even if paused
|
|
521
|
+
// This ensures accurate buffer duration metrics when user pauses during buffering
|
|
522
|
+
this.sendBufferEnd();
|
|
523
|
+
}
|
|
524
|
+
|
|
415
525
|
onPlaying() {
|
|
416
526
|
// Don't send CONTENT_RESUME if ads are playing (ads tracker handles it)
|
|
417
527
|
if (this.isAdsTrackerActive()) {
|
|
@@ -479,8 +589,31 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
479
589
|
this.sendStart();
|
|
480
590
|
}
|
|
481
591
|
}
|
|
592
|
+
|
|
593
|
+
onQualityLevelChange(e) {
|
|
594
|
+
if (this.qualityLevels && this.qualityLevels.selectedIndex >= 0) {
|
|
595
|
+
const selectedLevel =
|
|
596
|
+
this.qualityLevels[this.qualityLevels.selectedIndex];
|
|
597
|
+
if (selectedLevel) {
|
|
598
|
+
const newBitrate = selectedLevel.bitrate;
|
|
599
|
+
const oldBitrate = this.previousBitrate;
|
|
600
|
+
|
|
601
|
+
this.sendRenditionChanged({
|
|
602
|
+
oldBitrate,
|
|
603
|
+
newBitrate,
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
this.previousBitrate = newBitrate;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
482
610
|
}
|
|
483
611
|
|
|
612
|
+
// Attach AD_TRACKING and Log as static properties so UMD callers can use
|
|
613
|
+
// VideojsTracker.AD_TRACKING and VideojsTracker.Log without importing named exports.
|
|
614
|
+
VideojsTracker.AD_TRACKING = AD_TRACKING;
|
|
615
|
+
VideojsTracker.Log = nrvideo.Log;
|
|
616
|
+
|
|
484
617
|
// Static members
|
|
485
618
|
export {
|
|
486
619
|
HlsJsTech,
|
|
@@ -490,5 +623,6 @@ export {
|
|
|
490
623
|
ImaAdsTracker,
|
|
491
624
|
BrightcoveImaAdsTracker,
|
|
492
625
|
FreewheelAdsTracker,
|
|
626
|
+
DaiAdsTracker,
|
|
493
627
|
MediaTailorAdsTracker,
|
|
494
628
|
};
|