@newrelic/video-videojs 4.1.2 → 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.
- package/CHANGELOG.md +1 -1
- package/README.md +83 -41
- 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 +118 -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,53 @@ 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
|
+
|
|
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
|
+
|
|
20
68
|
nrvideo.Core.addTracker(this, options);
|
|
21
69
|
}
|
|
22
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
|
+
|
|
23
91
|
getTech() {
|
|
24
92
|
let tech = this.player.tech({ IWillNotUseThisInPlugins: true });
|
|
25
93
|
|
|
@@ -126,7 +194,8 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
126
194
|
if (tech?.vhs?.playlists?.media()) {
|
|
127
195
|
const activePlaylist = tech.vhs.playlists.media();
|
|
128
196
|
// Use AVERAGE-BANDWIDTH if available, fallback to BANDWIDTH
|
|
129
|
-
const bitrate =
|
|
197
|
+
const bitrate =
|
|
198
|
+
activePlaylist.attributes['AVERAGE-BANDWIDTH'] ||
|
|
130
199
|
activePlaylist.attributes.BANDWIDTH ||
|
|
131
200
|
null;
|
|
132
201
|
return bitrate !== null ? Math.round(bitrate) : null;
|
|
@@ -276,6 +345,7 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
276
345
|
this.onTimeupdate = this.onTimeupdate.bind(this);
|
|
277
346
|
this.OnAdsAllpodsCompleted = this.OnAdsAllpodsCompleted.bind(this);
|
|
278
347
|
this.onStreamManager = this.onStreamManager.bind(this);
|
|
348
|
+
this.onCanPlayThrough = this.onCanPlayThrough.bind(this);
|
|
279
349
|
|
|
280
350
|
this.player.on('loadstart', this.onDownload);
|
|
281
351
|
this.player.on('loadeddata', this.onDownload);
|
|
@@ -296,6 +366,7 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
296
366
|
this.player.on('timeupdate', this.onTimeupdate);
|
|
297
367
|
this.player.on('ads-allpods-completed', this.OnAdsAllpodsCompleted);
|
|
298
368
|
this.player.on('stream-manager', this.onStreamManager);
|
|
369
|
+
this.player.on('canplaythrough', this.onCanPlayThrough);
|
|
299
370
|
}
|
|
300
371
|
|
|
301
372
|
unregisterListeners() {
|
|
@@ -318,30 +389,31 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
318
389
|
this.player.off('timeupdate', this.onTimeupdate);
|
|
319
390
|
this.player.off('ads-allpods-completed', this.OnAdsAllpodsCompleted);
|
|
320
391
|
this.player.off('stream-manager', this.onStreamManager);
|
|
392
|
+
this.player.off('canplaythrough', this.onCanPlayThrough);
|
|
321
393
|
}
|
|
322
394
|
|
|
323
395
|
onDownload(e) {
|
|
324
396
|
this.sendDownload({ state: e.type });
|
|
325
397
|
|
|
326
|
-
// Check if MediaTailor should be used after the source is loaded
|
|
327
|
-
// Only check on 'loadstart' to avoid multiple checks
|
|
328
398
|
if (
|
|
399
|
+
this.adTracking === AD_TRACKING.SSAI.MT &&
|
|
329
400
|
!this.adsTracker &&
|
|
330
|
-
e.type === 'loadstart'
|
|
331
|
-
MediaTailorAdsTracker.isUsing(this.player)
|
|
401
|
+
e.type === 'loadstart'
|
|
332
402
|
) {
|
|
333
|
-
|
|
334
|
-
'VideojsTracker: Creating MediaTailorAdsTracker after source load'
|
|
335
|
-
);
|
|
403
|
+
nrvideo.Log.debug('VideojsTracker: Creating MediaTailorAdsTracker');
|
|
336
404
|
this.setAdsTracker(new MediaTailorAdsTracker(this.player, this.options));
|
|
337
|
-
// MediaTailor SSAI starts with content, not ads (unlike client-side
|
|
405
|
+
// MediaTailor SSAI starts with content, not ads (unlike client-side frameworks)
|
|
338
406
|
this.adsTracker.setIsAd(false);
|
|
339
407
|
}
|
|
340
408
|
}
|
|
341
409
|
|
|
342
410
|
// DAI methods
|
|
343
411
|
onStreamManager(event) {
|
|
344
|
-
if (
|
|
412
|
+
if (
|
|
413
|
+
this.adTracking === AD_TRACKING.SSAI.DAI &&
|
|
414
|
+
!this.adsTracker &&
|
|
415
|
+
event.StreamManager
|
|
416
|
+
) {
|
|
345
417
|
const daiTracker = new DaiAdsTracker(this.player);
|
|
346
418
|
daiTracker.setStreamManager(event.StreamManager);
|
|
347
419
|
this.setAdsTracker(daiTracker);
|
|
@@ -350,20 +422,29 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
350
422
|
// DAI methods end
|
|
351
423
|
|
|
352
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
|
+
|
|
353
436
|
if (!this.adsTracker) {
|
|
354
437
|
if (BrightcoveImaAdsTracker.isUsing(this.player)) {
|
|
355
|
-
|
|
438
|
+
nrvideo.Log.debug('VideojsTracker: auto-detected BrightcoveImaAdsTracker');
|
|
356
439
|
this.setAdsTracker(new BrightcoveImaAdsTracker(this.player));
|
|
357
440
|
} else if (ImaAdsTracker.isUsing(this.player)) {
|
|
358
|
-
|
|
441
|
+
nrvideo.Log.debug('VideojsTracker: auto-detected ImaAdsTracker');
|
|
359
442
|
this.setAdsTracker(new ImaAdsTracker(this.player));
|
|
360
443
|
} else if (FreewheelAdsTracker.isUsing(this.player)) {
|
|
361
|
-
|
|
362
|
-
|
|
444
|
+
nrvideo.Log.debug('VideojsTracker: auto-detected FreewheelAdsTracker');
|
|
363
445
|
this.setAdsTracker(new FreewheelAdsTracker(this.player));
|
|
364
|
-
// } else if (OnceAdsTracker.isUsing(this)) { // Once
|
|
365
446
|
} else {
|
|
366
|
-
|
|
447
|
+
nrvideo.Log.debug('VideojsTracker: no specific CSAI framework detected, using generic VideojsAdsTracker');
|
|
367
448
|
this.setAdsTracker(new VideojsAdsTracker(this.player));
|
|
368
449
|
}
|
|
369
450
|
}
|
|
@@ -393,6 +474,11 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
393
474
|
* @returns {boolean} True if ads are playing
|
|
394
475
|
*/
|
|
395
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)
|
|
396
482
|
return this.adsTracker && this.adsTracker.isAd && this.adsTracker.isAd();
|
|
397
483
|
}
|
|
398
484
|
|
|
@@ -412,6 +498,16 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
412
498
|
this.sendPause();
|
|
413
499
|
}
|
|
414
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
|
+
|
|
415
511
|
onPlaying() {
|
|
416
512
|
// Don't send CONTENT_RESUME if ads are playing (ads tracker handles it)
|
|
417
513
|
if (this.isAdsTrackerActive()) {
|
|
@@ -481,6 +577,11 @@ export default class VideojsTracker extends nrvideo.VideoTracker {
|
|
|
481
577
|
}
|
|
482
578
|
}
|
|
483
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
|
+
|
|
484
585
|
// Static members
|
|
485
586
|
export {
|
|
486
587
|
HlsJsTech,
|
|
@@ -490,5 +591,6 @@ export {
|
|
|
490
591
|
ImaAdsTracker,
|
|
491
592
|
BrightcoveImaAdsTracker,
|
|
492
593
|
FreewheelAdsTracker,
|
|
594
|
+
DaiAdsTracker,
|
|
493
595
|
MediaTailorAdsTracker,
|
|
494
596
|
};
|