@newrelic/video-core 4.1.8-beta → 5.0.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 +65 -15
- package/README.md +628 -45
- package/dist/cjs/browser/index.js +3 -0
- package/dist/cjs/browser/index.js.LICENSE.txt +6 -0
- package/dist/cjs/browser/index.js.map +1 -0
- 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/cjs/vega/index.js +3 -0
- package/dist/cjs/vega/index.js.LICENSE.txt +6 -0
- package/dist/cjs/vega/index.js.map +1 -0
- package/dist/esm/browser/index.js +3 -0
- package/dist/esm/browser/index.js.LICENSE.txt +6 -0
- package/dist/esm/browser/index.js.map +1 -0
- 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/esm/vega/index.js +3 -0
- package/dist/esm/vega/index.js.LICENSE.txt +6 -0
- package/dist/esm/vega/index.js.map +1 -0
- package/dist/umd/nrvideo.min.js +1 -1
- package/dist/umd/nrvideo.min.js.LICENSE.txt +1 -1
- package/dist/umd/nrvideo.min.js.map +1 -1
- package/package.json +31 -6
- package/src/{agent.js → browser/agent.js} +43 -8
- package/src/{harvestScheduler.js → browser/harvestScheduler.js} +87 -86
- package/src/browser/index.js +58 -0
- package/src/connectedDevice/connectedDeviceAgent.js +116 -0
- package/src/connectedDevice/connectedDeviceConstants.js +121 -0
- package/src/connectedDevice/connectedDeviceHarvester.js +522 -0
- package/src/connectedDevice/index.js +49 -0
- package/src/constants.js +8 -0
- package/src/core.js +4 -18
- package/src/eventAggregator.js +48 -0
- package/src/index.js +9 -10
- package/src/obfuscate.js +33 -0
- package/src/optimizedHttpClient.js +6 -2
- package/src/recordEvent.js +56 -49
- package/src/tracker.js +14 -4
- package/src/utils/eventBuilder.js +126 -0
- package/src/utils/harvestTimer.js +109 -0
- package/src/{utils.js → utils/index.js} +2 -2
- package/src/utils/qoeFilters.js +149 -0
- package/src/videoConfiguration.js +90 -7
- package/src/videotracker.js +100 -32
- package/src/videotrackerstate.js +156 -31
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QoE-related buffer + cycle helpers shared by both pipelines.
|
|
3
|
+
*
|
|
4
|
+
* All six helpers were previously duplicated between
|
|
5
|
+
* - `browser/agent.js` (`addEvent`, `refreshQoeKpis`)
|
|
6
|
+
* - `browser/harvestScheduler.js` (cycle filter, dirty splice, _qoeKpisUnchanged, _saveQoeKpis)
|
|
7
|
+
* - `connectedDevice/connectedDeviceHarvester.js` (same six)
|
|
8
|
+
*
|
|
9
|
+
* They're pure helpers — no class state. Callers pass in the buffer / snapshot
|
|
10
|
+
* map / event array. Keeping them shared makes the QoE behavior one source of
|
|
11
|
+
* truth across pipelines, so a fix landed once applies to both Browser and
|
|
12
|
+
* Vega (and any future CAF device pipeline).
|
|
13
|
+
*
|
|
14
|
+
* @module utils/qoeFilters
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import Constants from "../constants";
|
|
18
|
+
import Tracker from "../tracker";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Add an event to the buffer with QOE_AGGREGATE dedup. QOE_AGGREGATE events
|
|
22
|
+
* are deduplicated by `(actionName, viewId)` so multiple players sharing one
|
|
23
|
+
* harvester each get exactly one buffered QoE event per view. Non-QoE events
|
|
24
|
+
* are appended as-is, preserving the emit-time `timestamp` already set by
|
|
25
|
+
* `recordEvent.js`.
|
|
26
|
+
*
|
|
27
|
+
* @param {NrVideoEventAggregator} buffer
|
|
28
|
+
* @param {object} eventObject
|
|
29
|
+
* @returns {boolean} True if added/replaced.
|
|
30
|
+
*/
|
|
31
|
+
export function bufferEventWithQoeDedup(buffer, eventObject) {
|
|
32
|
+
if (!eventObject) return false;
|
|
33
|
+
if (eventObject.actionName === Tracker.Events.QOE_AGGREGATE) {
|
|
34
|
+
if (eventObject.viewId) {
|
|
35
|
+
return buffer.addOrReplaceByActionNameAndViewId(
|
|
36
|
+
Tracker.Events.QOE_AGGREGATE,
|
|
37
|
+
eventObject.viewId,
|
|
38
|
+
eventObject
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return buffer.addOrReplaceByActionName(Tracker.Events.QOE_AGGREGATE, eventObject);
|
|
42
|
+
}
|
|
43
|
+
return buffer.add(eventObject);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Update QoE KPI fields on the buffered QOE_AGGREGATE event for a given viewId.
|
|
48
|
+
* Looks up the existing buffered event, merges the fresh KPI values for keys
|
|
49
|
+
* listed in `Constants.QOE_KPI_KEYS`, and replaces it in the buffer. No-op if
|
|
50
|
+
* no QOE_AGGREGATE event is currently buffered.
|
|
51
|
+
*
|
|
52
|
+
* @param {NrVideoEventAggregator} buffer
|
|
53
|
+
* @param {object} freshKpis
|
|
54
|
+
* @param {string} [viewId]
|
|
55
|
+
*/
|
|
56
|
+
export function refreshQoeKpisInBuffer(buffer, freshKpis, viewId) {
|
|
57
|
+
if (!buffer || !freshKpis) return;
|
|
58
|
+
const existing = viewId
|
|
59
|
+
? buffer.findByActionNameAndViewId(Tracker.Events.QOE_AGGREGATE, viewId)
|
|
60
|
+
: buffer.findByActionName(Tracker.Events.QOE_AGGREGATE);
|
|
61
|
+
if (!existing) return;
|
|
62
|
+
const updated = { ...existing };
|
|
63
|
+
for (const key of Constants.QOE_KPI_KEYS) {
|
|
64
|
+
if (key in freshKpis) updated[key] = freshKpis[key];
|
|
65
|
+
}
|
|
66
|
+
if (viewId) {
|
|
67
|
+
buffer.addOrReplaceByActionNameAndViewId(Tracker.Events.QOE_AGGREGATE, viewId, updated);
|
|
68
|
+
} else {
|
|
69
|
+
buffer.addOrReplaceByActionName(Tracker.Events.QOE_AGGREGATE, updated);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Cross-cycle dirty check — true iff every KPI field on `event` equals the
|
|
75
|
+
* snapshot saved for that event's viewId.
|
|
76
|
+
*
|
|
77
|
+
* @param {Object<string, object>} snapshots - viewId → KPI snapshot
|
|
78
|
+
* @param {object} event
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
export function qoeKpisUnchanged(snapshots, event) {
|
|
82
|
+
const snapshot = snapshots[event.viewId];
|
|
83
|
+
if (!snapshot) return false;
|
|
84
|
+
for (const key of Constants.QOE_KPI_KEYS) {
|
|
85
|
+
if (event[key] !== snapshot[key]) return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Save a snapshot of an event's QoE KPI fields, keyed by viewId, for the next
|
|
92
|
+
* cross-cycle dirty check.
|
|
93
|
+
*
|
|
94
|
+
* @param {Object<string, object>} snapshots - viewId → KPI snapshot (mutated in place)
|
|
95
|
+
* @param {object} event
|
|
96
|
+
*/
|
|
97
|
+
export function saveQoeKpiSnapshot(snapshots, event) {
|
|
98
|
+
const snapshot = {};
|
|
99
|
+
for (const key of Constants.QOE_KPI_KEYS) {
|
|
100
|
+
snapshot[key] = event[key];
|
|
101
|
+
}
|
|
102
|
+
snapshots[event.viewId] = snapshot;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Partition drained events by the QoE-cycle filter. On a QoE cycle, all events
|
|
107
|
+
* pass through. On a non-QoE cycle, QOE_AGGREGATE events are re-buffered (so
|
|
108
|
+
* they ship on the next QoE cycle) and only non-QoE events are returned.
|
|
109
|
+
*
|
|
110
|
+
* @param {object[]} drained
|
|
111
|
+
* @param {boolean} isQoeCycle
|
|
112
|
+
* @param {NrVideoEventAggregator} buffer
|
|
113
|
+
* @returns {object[]}
|
|
114
|
+
*/
|
|
115
|
+
export function partitionByQoeCycle(drained, isQoeCycle, buffer) {
|
|
116
|
+
if (isQoeCycle) return drained;
|
|
117
|
+
const filtered = [];
|
|
118
|
+
for (const e of drained) {
|
|
119
|
+
if (e.actionName === Tracker.Events.QOE_AGGREGATE) {
|
|
120
|
+
buffer.add(e);
|
|
121
|
+
} else {
|
|
122
|
+
filtered.push(e);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return filtered;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Cross-cycle dirty splice. Walks the array in reverse and drops any
|
|
130
|
+
* QOE_AGGREGATE event whose KPI fields are unchanged since the last send (so
|
|
131
|
+
* we don't waste bytes shipping identical aggregates). Saves a fresh snapshot
|
|
132
|
+
* for any QOE_AGGREGATE that does ship. Mutates `filtered` in place.
|
|
133
|
+
*
|
|
134
|
+
* @param {object[]} filtered
|
|
135
|
+
* @param {Object<string, object>} snapshots - viewId → KPI snapshot (read+written)
|
|
136
|
+
* @param {boolean} isForced - When true, skip the dirty check and ship every
|
|
137
|
+
* QOE_AGGREGATE regardless of KPI sameness (final flush, CONTENT_END, etc.)
|
|
138
|
+
*/
|
|
139
|
+
export function applyQoeDirtyFilter(filtered, snapshots, isForced) {
|
|
140
|
+
for (let i = filtered.length - 1; i >= 0; i--) {
|
|
141
|
+
const e = filtered[i];
|
|
142
|
+
if (e.actionName !== Tracker.Events.QOE_AGGREGATE) continue;
|
|
143
|
+
if (!isForced && qoeKpisUnchanged(snapshots, e)) {
|
|
144
|
+
filtered.splice(i, 1);
|
|
145
|
+
} else {
|
|
146
|
+
saveQoeKpiSnapshot(snapshots, e);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -15,18 +15,42 @@ class VideoConfiguration {
|
|
|
15
15
|
* @returns {boolean} True if configuration is valid and set
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
setConfiguration(userInfo, config) {
|
|
19
|
-
|
|
18
|
+
setConfiguration(userInfo, config, src) {
|
|
19
|
+
const validated = src === "Vega"
|
|
20
|
+
? this.validateVegaFields(userInfo)
|
|
21
|
+
: this.validateRequiredFields(userInfo);
|
|
22
|
+
if (!validated) {
|
|
20
23
|
return false;
|
|
21
24
|
}
|
|
22
25
|
if (!this.validateConfigFields(config)) {
|
|
23
26
|
return false;
|
|
24
27
|
}
|
|
25
|
-
this.initializeGlobalConfig(userInfo, config);
|
|
28
|
+
this.initializeGlobalConfig(userInfo, config, src);
|
|
26
29
|
Log.notice("Video analytics configuration initialized successfully");
|
|
27
30
|
return true;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Validates required Vega configuration fields.
|
|
35
|
+
* @param {object} info
|
|
36
|
+
* @returns {boolean} True if valid
|
|
37
|
+
*/
|
|
38
|
+
validateVegaFields(info) {
|
|
39
|
+
if (!info || typeof info !== "object") {
|
|
40
|
+
Log.error("Configuration must be an object");
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (!info.applicationToken) {
|
|
44
|
+
Log.error("applicationToken is required");
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (!["US", "EU", "staging", "GOV"].includes(info.endpoint)) {
|
|
48
|
+
Log.error("Invalid endpoint (must be US, EU, staging, or GOV)");
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
/**
|
|
31
55
|
* Validates required configuration fields.
|
|
32
56
|
* @param {object} config - Configuration to validate
|
|
@@ -92,22 +116,79 @@ class VideoConfiguration {
|
|
|
92
116
|
return false;
|
|
93
117
|
}
|
|
94
118
|
|
|
95
|
-
const { qoeAggregate } = config;
|
|
119
|
+
const { qoeAggregate, obfuscate } = config;
|
|
96
120
|
|
|
97
121
|
if (qoeAggregate !== undefined && typeof qoeAggregate !== "boolean") {
|
|
98
122
|
Log.error("qoeAggregate must be a boolean");
|
|
99
123
|
return false;
|
|
100
124
|
}
|
|
101
125
|
|
|
126
|
+
if (obfuscate !== undefined && !Array.isArray(obfuscate)) {
|
|
127
|
+
Log.error("obfuscate must be an array");
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
102
131
|
return true;
|
|
103
132
|
}
|
|
104
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Filters obfuscation rules, warning about and removing invalid ones.
|
|
136
|
+
* @param {Array} rules - Raw obfuscation rules
|
|
137
|
+
* @returns {Array} Valid rules only
|
|
138
|
+
*/
|
|
139
|
+
filterObfuscateRules(rules) {
|
|
140
|
+
if (!rules) return [];
|
|
141
|
+
return rules.filter((rule) => {
|
|
142
|
+
const hasRegex = rule.regex !== undefined && (typeof rule.regex === "string" || rule.regex instanceof RegExp);
|
|
143
|
+
const hasReplacement = rule.replacement !== undefined && typeof rule.replacement === "string";
|
|
144
|
+
if (!hasRegex || !hasReplacement) {
|
|
145
|
+
Log.warn("obfuscate rule missing required 'regex' (string|RegExp) and/or 'replacement' (string), skipping:", rule);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Sanitizes qoeIntervalFactor, defaulting to Constants.DEFAULT_QOE_INTERVAL_FACTOR
|
|
154
|
+
* if the value is not a positive integer.
|
|
155
|
+
* @param {*} value
|
|
156
|
+
* @returns {number}
|
|
157
|
+
*/
|
|
158
|
+
sanitizeQoeIntervalFactor(value) {
|
|
159
|
+
if (value === undefined || value === null) return Constants.DEFAULT_QOE_INTERVAL_FACTOR;
|
|
160
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
|
|
161
|
+
Log.warn(`Invalid qoeIntervalFactor "${value}" — must be a positive integer. Defaulting to ${Constants.DEFAULT_QOE_INTERVAL_FACTOR}.`);
|
|
162
|
+
return Constants.DEFAULT_QOE_INTERVAL_FACTOR;
|
|
163
|
+
}
|
|
164
|
+
|
|
105
165
|
/**
|
|
106
166
|
* Initializes the global NRVIDEO configuration object.
|
|
107
167
|
* @param {object} userInfo - User provided configuration
|
|
108
168
|
* @param {object} [config] - Optional configuration object
|
|
109
169
|
*/
|
|
110
|
-
initializeGlobalConfig(userInfo, config) {
|
|
170
|
+
initializeGlobalConfig(userInfo, config, src) {
|
|
171
|
+
// Vega path: write `globalThis.__NRVIDEO_CD__` with info+config only.
|
|
172
|
+
// The harvester is owned by `connectedDeviceAgent.js` as a module singleton — no
|
|
173
|
+
// harvester field on this global.
|
|
174
|
+
if (src === "Vega") {
|
|
175
|
+
globalThis.__NRVIDEO_CD__ = {
|
|
176
|
+
info: {
|
|
177
|
+
accountId: userInfo.accountId,
|
|
178
|
+
applicationToken: userInfo.applicationToken,
|
|
179
|
+
endpoint: userInfo.endpoint,
|
|
180
|
+
...(userInfo.appName ? { appName: userInfo.appName } : {}),
|
|
181
|
+
...(userInfo.applicationID ? { applicationID: userInfo.applicationID } : {}),
|
|
182
|
+
...(userInfo.deviceInfo ? { deviceInfo: userInfo.deviceInfo } : {}),
|
|
183
|
+
},
|
|
184
|
+
config: {
|
|
185
|
+
qoeAggregate: config?.qoeAggregate ?? true,
|
|
186
|
+
qoeIntervalFactor: this.sanitizeQoeIntervalFactor(config?.qoeIntervalFactor),
|
|
187
|
+
obfuscate: this.filterObfuscateRules(config?.obfuscate),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
111
192
|
|
|
112
193
|
let { licenseKey, appName, region, beacon, applicationID } = userInfo;
|
|
113
194
|
|
|
@@ -128,6 +209,8 @@ class VideoConfiguration {
|
|
|
128
209
|
},
|
|
129
210
|
config: {
|
|
130
211
|
qoeAggregate: config?.qoeAggregate ?? true,
|
|
212
|
+
qoeIntervalFactor: this.sanitizeQoeIntervalFactor(config?.qoeIntervalFactor),
|
|
213
|
+
obfuscate: this.filterObfuscateRules(config?.obfuscate),
|
|
131
214
|
}
|
|
132
215
|
};
|
|
133
216
|
}
|
|
@@ -142,8 +225,8 @@ const videoConfiguration = new VideoConfiguration();
|
|
|
142
225
|
* @param {object} [config] - Optional configuration object
|
|
143
226
|
* @returns {boolean} True if configuration was set successfully
|
|
144
227
|
*/
|
|
145
|
-
export function setVideoConfig(info, config) {
|
|
146
|
-
return videoConfiguration.setConfiguration(info, config);
|
|
228
|
+
export function setVideoConfig(info, config, src) {
|
|
229
|
+
return videoConfiguration.setConfiguration(info, config, src);
|
|
147
230
|
}
|
|
148
231
|
|
|
149
232
|
export { videoConfiguration };
|
package/src/videotracker.js
CHANGED
|
@@ -44,6 +44,7 @@ class VideoTracker extends Tracker {
|
|
|
44
44
|
*/
|
|
45
45
|
this._lastBufferType = null;
|
|
46
46
|
this._userId = null;
|
|
47
|
+
this._src = null;
|
|
47
48
|
|
|
48
49
|
options = options || {};
|
|
49
50
|
this.setOptions(options);
|
|
@@ -83,6 +84,16 @@ class VideoTracker extends Tracker {
|
|
|
83
84
|
if (typeof options.isAd === "boolean") {
|
|
84
85
|
this.setIsAd(options.isAd);
|
|
85
86
|
}
|
|
87
|
+
if (options.src !== undefined) {
|
|
88
|
+
if (this._src !== null && this._src !== options.src) {
|
|
89
|
+
Log.warn(`setOptions: src is locked to '${this._src}'. Ignoring override '${options.src}'.`);
|
|
90
|
+
} else {
|
|
91
|
+
this._src = options.src;
|
|
92
|
+
if (this.adsTracker && this.adsTracker._src == null) {
|
|
93
|
+
this.adsTracker._src = this._src;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
86
97
|
Tracker.prototype.setOptions.apply(this, arguments);
|
|
87
98
|
}
|
|
88
99
|
}
|
|
@@ -134,6 +145,12 @@ class VideoTracker extends Tracker {
|
|
|
134
145
|
this.adsTracker = tracker;
|
|
135
146
|
this.adsTracker.setIsAd(true);
|
|
136
147
|
this.adsTracker.parentTracker = this;
|
|
148
|
+
// Propagate src so ads events route to the same pipeline as content events.
|
|
149
|
+
// Covers the case where setAdsTracker is called after the content tracker
|
|
150
|
+
// is fully initialised and this._src is already set.
|
|
151
|
+
if (this._src != null && tracker._src == null) {
|
|
152
|
+
tracker._src = this._src;
|
|
153
|
+
}
|
|
137
154
|
this.adsTracker.on("*", funnelAdEvents.bind(this));
|
|
138
155
|
}
|
|
139
156
|
}
|
|
@@ -239,6 +256,21 @@ class VideoTracker extends Tracker {
|
|
|
239
256
|
return null;
|
|
240
257
|
}
|
|
241
258
|
|
|
259
|
+
/** Override to return the manifest-declared bitrate in bps (Indicated Bitrate). */
|
|
260
|
+
getManifestBitrate() {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Override to return the measured network bitrate in bps (Observed Bitrate). */
|
|
265
|
+
getSegmentDownloadBitrate() {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Override to return the download throughput in bps. */
|
|
270
|
+
getNetworkDownloadBitrate() {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
242
274
|
/** Calculates consumed bitrate using webkitVideoDecodedByteCount. */
|
|
243
275
|
getWebkitBitrate() {
|
|
244
276
|
if (this.tag && this.tag.webkitVideoDecodedByteCount) {
|
|
@@ -273,7 +305,7 @@ class VideoTracker extends Tracker {
|
|
|
273
305
|
* saving the current rendition and thus preventing interferences with RENDITION_CHANGE events.
|
|
274
306
|
*/
|
|
275
307
|
getRenditionShift(saveNewRendition) {
|
|
276
|
-
let current = this.
|
|
308
|
+
let current = this.getManifestBitrate();
|
|
277
309
|
let last;
|
|
278
310
|
if (this.isAd()) {
|
|
279
311
|
last = this._lastAdRendition;
|
|
@@ -449,7 +481,7 @@ class VideoTracker extends Tracker {
|
|
|
449
481
|
att["instrumentation.name"] = this.getInstrumentationName();
|
|
450
482
|
att["instrumentation.version"] = this.getInstrumentationVersion();
|
|
451
483
|
att["enduser.id"] = this._userId;
|
|
452
|
-
att["src"] = "Browser";
|
|
484
|
+
att["src"] = this._src || "Browser";
|
|
453
485
|
|
|
454
486
|
if (type === "customAction") return att;
|
|
455
487
|
|
|
@@ -465,12 +497,14 @@ class VideoTracker extends Tracker {
|
|
|
465
497
|
att.adTitle = this.getTitle();
|
|
466
498
|
att.adSrc = this.getSrc();
|
|
467
499
|
att.adCdn = this.getCdn();
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
500
|
+
|
|
501
|
+
// Only add bitrate attributes after ad has started
|
|
502
|
+
if (this.state.isStarted) {
|
|
503
|
+
att.adBitrate =
|
|
504
|
+
this.getBitrate() || 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
472
507
|
att.adRenditionName = this.getRenditionName();
|
|
473
|
-
att.adRenditionBitrate = this.getRenditionBitrate();
|
|
474
508
|
att.adRenditionHeight = this.getRenditionHeight();
|
|
475
509
|
att.adRenditionWidth = this.getRenditionWidth();
|
|
476
510
|
att.adDuration = this.getDuration();
|
|
@@ -492,13 +526,16 @@ class VideoTracker extends Tracker {
|
|
|
492
526
|
att.contentPlayhead = this.getPlayhead();
|
|
493
527
|
|
|
494
528
|
att.contentIsLive = this.isLive();
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
this.
|
|
529
|
+
|
|
530
|
+
// Only add bitrate attributes after content has started
|
|
531
|
+
if (this.state.isStarted) {
|
|
532
|
+
att.contentBitrate = this.getBitrate()|| 0;
|
|
533
|
+
att.contentManifestBitrate = this.getManifestBitrate() || 0;
|
|
534
|
+
att.contentSegmentDownloadBitrate = this.getSegmentDownloadBitrate() || 0;
|
|
535
|
+
att.contentNetworkDownloadBitrate = this.getNetworkDownloadBitrate() || 0;
|
|
536
|
+
}
|
|
499
537
|
|
|
500
538
|
att.contentRenditionName = this.getRenditionName();
|
|
501
|
-
att.contentRenditionBitrate = this.getRenditionBitrate();
|
|
502
539
|
att.contentRenditionHeight = this.getRenditionHeight();
|
|
503
540
|
att.contentRenditionWidth = this.getRenditionWidth();
|
|
504
541
|
att.contentDuration = this.getDuration();
|
|
@@ -523,26 +560,24 @@ class VideoTracker extends Tracker {
|
|
|
523
560
|
|
|
524
561
|
if(this.state.isStarted && !this.isAd()) {
|
|
525
562
|
this.state.trackContentBitrateState(att.contentBitrate);
|
|
563
|
+
this.state.trackDownloadRate(att.contentNetworkDownloadBitrate);
|
|
564
|
+
this.state.addPlayedRendition(att.contentRenditionHeight, att.contentRenditionWidth);
|
|
526
565
|
}
|
|
527
566
|
|
|
528
567
|
for (let key in this.customData) {
|
|
529
568
|
att[key] = this.customData[key];
|
|
530
569
|
}
|
|
531
570
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
this.addQoeAttributes(att);
|
|
536
|
-
|
|
537
|
-
return att;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
addQoeAttributes(att) {
|
|
541
|
-
att = this.state.getQoeAttributes(att);
|
|
571
|
+
// Add QOE v1.1 attributes (for content only, not ads)
|
|
572
|
+
if (!this.isAd()) {
|
|
573
|
+
this.state.getQoeAttributes(att);
|
|
542
574
|
const qoe = att.qoe;
|
|
543
575
|
for (let key in this.customData) {
|
|
544
|
-
|
|
576
|
+
qoe[key] = this.customData[key];
|
|
545
577
|
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return att;
|
|
546
581
|
}
|
|
547
582
|
|
|
548
583
|
/**
|
|
@@ -610,15 +645,23 @@ class VideoTracker extends Tracker {
|
|
|
610
645
|
if(this.adsTracker) {
|
|
611
646
|
// If ads state is set to playing (ad error) after content start, reset the ad state.
|
|
612
647
|
if(this.adsTracker.state.isPlaying || this.adsTracker.state.isBuffering) {
|
|
613
|
-
|
|
648
|
+
this.adsTracker.state.stopAdsTime();
|
|
614
649
|
this.adsTracker.state.isPlaying = false;
|
|
615
650
|
this.adsTracker.state.isBuffering = false;
|
|
616
|
-
} else {
|
|
617
|
-
totalAdsTime = this.adsTracker.state.totalAdTime() ?? 0;
|
|
618
651
|
}
|
|
652
|
+
// Always use totalAdTime() which includes accumulator across all ads
|
|
653
|
+
totalAdsTime = this.adsTracker.state.totalAdTime() ?? 0;
|
|
619
654
|
}
|
|
620
655
|
this.state.setStartupTime(totalAdsTime)
|
|
621
656
|
this.sendVideoAction(ev, att);
|
|
657
|
+
|
|
658
|
+
const harvester = this.getHarvester?.();
|
|
659
|
+
harvester?.setBeforeDrainCallback(() => {
|
|
660
|
+
if (this.state) {
|
|
661
|
+
const freshKpis = this.state.getQoeAttributes({}).qoe;
|
|
662
|
+
harvester.refreshQoeKpis(freshKpis, this.getViewId());
|
|
663
|
+
}
|
|
664
|
+
});
|
|
622
665
|
}
|
|
623
666
|
//this.send(ev, att);
|
|
624
667
|
this.startHeartbeat();
|
|
@@ -640,7 +683,10 @@ class VideoTracker extends Tracker {
|
|
|
640
683
|
ev = VideoTracker.Events.AD_END;
|
|
641
684
|
att.timeSinceAdRequested = this.state.timeSinceRequested.getDeltaTime();
|
|
642
685
|
att.timeSinceAdStarted = this.state.timeSinceStarted.getDeltaTime();
|
|
643
|
-
if (this.parentTracker)
|
|
686
|
+
if (this.parentTracker) {
|
|
687
|
+
this.parentTracker.state.isPlaying = true;
|
|
688
|
+
this.parentTracker.state.timeSincePaused.reset();
|
|
689
|
+
}
|
|
644
690
|
this.state.stopAdsTime();
|
|
645
691
|
} else {
|
|
646
692
|
ev = VideoTracker.Events.CONTENT_END;
|
|
@@ -658,9 +704,15 @@ class VideoTracker extends Tracker {
|
|
|
658
704
|
this.state.goViewCountUp();
|
|
659
705
|
this.state.totalPlaytime = 0;
|
|
660
706
|
if(!this.isAd()) {
|
|
707
|
+
const harvester = this.getHarvester?.();
|
|
708
|
+
// Force QoE to be included in the next harvest cycle at content end
|
|
709
|
+
harvester?.forceNextQoeCycle?.();
|
|
710
|
+
// Clear the before-drain callback so the next harvest doesn't overwrite
|
|
711
|
+
// the final QoE (already in buffer) with zeroed-out state values
|
|
712
|
+
harvester?.setBeforeDrainCallback?.(null);
|
|
661
713
|
// reset the states after the view count is up
|
|
662
|
-
|
|
663
|
-
|
|
714
|
+
if(this.adsTracker) this.adsTracker.state.clearTotalAdsTime();
|
|
715
|
+
this.state.resetViewIdTrackedState();
|
|
664
716
|
}
|
|
665
717
|
}
|
|
666
718
|
}
|
|
@@ -679,8 +731,6 @@ class VideoTracker extends Tracker {
|
|
|
679
731
|
this.isAd()
|
|
680
732
|
? this.sendVideoAdAction(ev, att)
|
|
681
733
|
: this.sendVideoAction(ev, att);
|
|
682
|
-
|
|
683
|
-
//this.send(ev, att);
|
|
684
734
|
}
|
|
685
735
|
}
|
|
686
736
|
|
|
@@ -865,7 +915,25 @@ class VideoTracker extends Tracker {
|
|
|
865
915
|
att = att || {};
|
|
866
916
|
att.timeSinceLastRenditionChange =
|
|
867
917
|
this.state.timeSinceLastRenditionChange.getDeltaTime();
|
|
868
|
-
|
|
918
|
+
|
|
919
|
+
const {oldBitrate, newBitrate} = att;
|
|
920
|
+
|
|
921
|
+
if (!this.isAd() && oldBitrate !== undefined && newBitrate !== undefined) {
|
|
922
|
+
if (newBitrate > oldBitrate) {
|
|
923
|
+
att.shift = "up";
|
|
924
|
+
this.state.totalSwitchUps += 1;
|
|
925
|
+
} else if (newBitrate < oldBitrate) {
|
|
926
|
+
att.shift = "down";
|
|
927
|
+
this.state.totalSwitchDowns += 1;
|
|
928
|
+
} else {
|
|
929
|
+
att.shift = null;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Remove internal tracking fields before sending to analytics
|
|
934
|
+
delete att.oldBitrate;
|
|
935
|
+
delete att.newBitrate;
|
|
936
|
+
|
|
869
937
|
let ev;
|
|
870
938
|
if (this.isAd()) {
|
|
871
939
|
ev = VideoTracker.Events.AD_RENDITION_CHANGE;
|