@newrelic/video-core 4.1.3-beta → 4.1.3
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 +31 -8
- package/README.md +13 -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/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/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 +1 -1
- package/src/agent.js +42 -1
- package/src/constants.js +11 -1
- package/src/core.js +1 -1
- package/src/eventAggregator.js +10 -0
- package/src/harvestScheduler.js +84 -3
- package/src/recordEvent.js +1 -1
- package/src/videoConfiguration.js +55 -7
- package/src/videotracker.js +49 -13
- package/src/videotrackerstate.js +72 -21
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HarvestScheduler } from "./harvestScheduler.js";
|
|
2
2
|
import { NrVideoEventAggregator } from "./eventAggregator.js";
|
|
3
|
+
import Constants from "./constants.js";
|
|
3
4
|
import Log from "./log.js";
|
|
4
5
|
import Tracker from "./tracker";
|
|
5
6
|
|
|
@@ -49,7 +50,8 @@ class VideoAnalyticsAgent {
|
|
|
49
50
|
|
|
50
51
|
try {
|
|
51
52
|
if(eventObject.actionName && eventObject.actionName === Tracker.Events.QOE_AGGREGATE) {
|
|
52
|
-
//
|
|
53
|
+
// Ensure only one QOE_AGGREGATE event exists in buffer per harvest cycle.
|
|
54
|
+
// Dirty-check dedup happens at drain time in HarvestScheduler._qoeKpisUnchanged().
|
|
53
55
|
return this.eventBuffer.addOrReplaceByActionName(Tracker.Events.QOE_AGGREGATE, eventObject);
|
|
54
56
|
}
|
|
55
57
|
return this.eventBuffer.add(eventObject);
|
|
@@ -71,6 +73,45 @@ class VideoAnalyticsAgent {
|
|
|
71
73
|
|
|
72
74
|
this.harvestScheduler.updateHarvestInterval(interval);
|
|
73
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Forces the next harvest cycle to include QOE_AGGREGATE events.
|
|
79
|
+
* Called at CONTENT_END to ensure final QoE is sent.
|
|
80
|
+
*/
|
|
81
|
+
forceNextQoeCycle() {
|
|
82
|
+
if (this.harvestScheduler) {
|
|
83
|
+
this.harvestScheduler.forceNextQoeCycle = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Sets a callback to be called before each drain to refresh QoE KPIs.
|
|
89
|
+
* @param {Function|null} callback - Function that refreshes QoE data in the buffer, or null to clear
|
|
90
|
+
*/
|
|
91
|
+
setBeforeDrainCallback(callback) {
|
|
92
|
+
if (this.harvestScheduler) {
|
|
93
|
+
this.harvestScheduler.beforeDrainCallback = callback;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Updates QoE KPI fields on the existing QOE_AGGREGATE event in the buffer.
|
|
99
|
+
* Uses addOrReplaceByActionName to keep payload size tracking accurate.
|
|
100
|
+
* @param {object} freshKpis - Object with latest KPI values
|
|
101
|
+
*/
|
|
102
|
+
refreshQoeKpis(freshKpis) {
|
|
103
|
+
if (!this.eventBuffer || !freshKpis) return;
|
|
104
|
+
const existing = this.eventBuffer.findByActionName(Tracker.Events.QOE_AGGREGATE);
|
|
105
|
+
if (existing) {
|
|
106
|
+
const updated = { ...existing };
|
|
107
|
+
for (const key of Constants.QOE_KPI_KEYS) {
|
|
108
|
+
if (key in freshKpis) {
|
|
109
|
+
updated[key] = freshKpis[key];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.eventBuffer.addOrReplaceByActionName(Tracker.Events.QOE_AGGREGATE, updated);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
74
115
|
}
|
|
75
116
|
|
|
76
117
|
// Create singleton instance
|
package/src/constants.js
CHANGED
|
@@ -42,10 +42,20 @@ Constants.MAX_BEACON_SIZE = 61440; // 60KB = 60 × 1024 bytes
|
|
|
42
42
|
Constants.MAX_EVENTS_PER_BATCH = 1000;
|
|
43
43
|
Constants.INTERVAL = 10000; //10 seconds
|
|
44
44
|
|
|
45
|
+
Constants.QOE_KPI_KEYS = [
|
|
46
|
+
"startupTime", "peakBitrate", "averageBitrate", "totalPlaytime",
|
|
47
|
+
"totalRebufferingTime", "rebufferingRatio", "hadStartupError",
|
|
48
|
+
"hadPlaybackError", "numberOfErrors"
|
|
49
|
+
];
|
|
50
|
+
|
|
45
51
|
Constants.QOE_AGGREGATE_KEYS = [
|
|
46
52
|
"coreVersion", "instrumentation.name",
|
|
47
53
|
"instrumentation.provider", "instrumentation.version", "isBackgroundEvent", "playerName", "playerVersion",
|
|
48
|
-
"src", "viewId", "viewSession", "contentIsAutoplayed"
|
|
54
|
+
"src", "viewId", "viewSession", "contentIsAutoplayed", "contentIsMuted", "contentRenditionHeight", "contentRenditionWidth",
|
|
55
|
+
"contentSrc", "numberOfVideos", "pageUrl", "trackerName", "trackerVersion", "contentDuration", "contentPlayrate", "contentPlayhead",
|
|
56
|
+
"contentPreload", "elapsedTime", "contentTitle", "contentId", "contentIsLive", "deviceType", "deviceGroup", "deviceManufacturer",
|
|
57
|
+
"deviceModel", "deviceName", "deviceSize", "deviceUuid", "contentRenditionName", "contentIsFullscreen", "contentCdn",
|
|
58
|
+
"contentFps", "asnOrganization", "asnLongitude", "asnLatitude", "asn", "timeSinceRequested", "timeSinceStarted"
|
|
49
59
|
]
|
|
50
60
|
|
|
51
61
|
export default Constants;
|
package/src/core.js
CHANGED
package/src/eventAggregator.js
CHANGED
|
@@ -55,6 +55,16 @@ export class NrVideoEventAggregator {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Returns the existing event in buffer matching the given actionName, or null.
|
|
60
|
+
* @param {string} actionName
|
|
61
|
+
* @returns {object|null}
|
|
62
|
+
*/
|
|
63
|
+
findByActionName(actionName) {
|
|
64
|
+
const event = this.buffer.find(e => e.actionName === actionName);
|
|
65
|
+
return event || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
/**
|
|
59
69
|
* Adds an event to the unified buffer.
|
|
60
70
|
* All events are treated equally in FIFO order.
|
package/src/harvestScheduler.js
CHANGED
|
@@ -3,6 +3,7 @@ import { RetryQueueHandler } from "./retryQueueHandler";
|
|
|
3
3
|
import { OptimizedHttpClient } from "./optimizedHttpClient";
|
|
4
4
|
import { buildUrl, dataSize } from "./utils";
|
|
5
5
|
import Constants from "./constants";
|
|
6
|
+
import Tracker from "./tracker";
|
|
6
7
|
import Log from "./log";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -31,6 +32,10 @@ export class HarvestScheduler {
|
|
|
31
32
|
this.currentTimerId = null;
|
|
32
33
|
this.harvestCycle = Constants.INTERVAL;
|
|
33
34
|
this.isHarvesting = false;
|
|
35
|
+
this.qoeCycleCount = 1;
|
|
36
|
+
this.forceNextQoeCycle = false;
|
|
37
|
+
this.beforeDrainCallback = null;
|
|
38
|
+
this._lastSentQoeKpis = null;
|
|
34
39
|
|
|
35
40
|
// Page lifecycle handling
|
|
36
41
|
this.setupPageLifecycleHandlers();
|
|
@@ -237,15 +242,65 @@ export class HarvestScheduler {
|
|
|
237
242
|
/**
|
|
238
243
|
* Drains events from the event buffer and optionally includes retry queue data.
|
|
239
244
|
* Uses fresh-events-first approach with payload limits.
|
|
245
|
+
* Filters out QOE_AGGREGATE events based on the harvest interval multiplier,
|
|
246
|
+
* always including them on the first and final harvest cycles.
|
|
240
247
|
* @param {object} options - Harvest options
|
|
241
248
|
* @returns {Array} Drained events
|
|
242
249
|
* @private
|
|
243
250
|
*/
|
|
244
|
-
drainEvents() {
|
|
251
|
+
drainEvents(options = {}) {
|
|
252
|
+
// Determine if this cycle should include the QOE_AGGREGATE event
|
|
253
|
+
const multiplier = window.NRVIDEO?.config?.qoeIntervalFactor ?? 1;
|
|
254
|
+
const isForced = !!options.isFinalHarvest || this.forceNextQoeCycle;
|
|
255
|
+
const isQoeCycle =
|
|
256
|
+
(this.qoeCycleCount - 1) % multiplier === 0 || isForced;
|
|
257
|
+
|
|
258
|
+
// Reset force flag after use
|
|
259
|
+
if (this.forceNextQoeCycle) this.forceNextQoeCycle = false;
|
|
260
|
+
|
|
261
|
+
// Refresh QoE KPIs with latest tracker state before draining
|
|
262
|
+
if (this.beforeDrainCallback && typeof this.beforeDrainCallback === 'function') {
|
|
263
|
+
try {
|
|
264
|
+
this.beforeDrainCallback();
|
|
265
|
+
} catch (e) {
|
|
266
|
+
Log.error("Before drain callback failed:", e.message);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
245
270
|
// Always drain fresh events first (priority approach)
|
|
246
271
|
const freshEvents = this.eventBuffer.drain();
|
|
247
|
-
let
|
|
248
|
-
|
|
272
|
+
let filteredFreshEvents;
|
|
273
|
+
if (isQoeCycle) {
|
|
274
|
+
filteredFreshEvents = freshEvents;
|
|
275
|
+
} else {
|
|
276
|
+
// On non-QoE cycles, put QoE events back into buffer instead of losing them
|
|
277
|
+
filteredFreshEvents = [];
|
|
278
|
+
for (const e of freshEvents) {
|
|
279
|
+
if (e.actionName === Tracker.Events.QOE_AGGREGATE) {
|
|
280
|
+
this.eventBuffer.add(e);
|
|
281
|
+
} else {
|
|
282
|
+
filteredFreshEvents.push(e);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.qoeCycleCount++;
|
|
288
|
+
|
|
289
|
+
// Cross-cycle dirty check: skip QoE if KPIs unchanged since last send
|
|
290
|
+
// Forced cycles (CONTENT_END, page unload) always send QoE
|
|
291
|
+
for (let i = filteredFreshEvents.length - 1; i >= 0; i--) {
|
|
292
|
+
const e = filteredFreshEvents[i];
|
|
293
|
+
if (e.actionName === Tracker.Events.QOE_AGGREGATE) {
|
|
294
|
+
if (!isForced && this._qoeKpisUnchanged(e)) {
|
|
295
|
+
filteredFreshEvents.splice(i, 1);
|
|
296
|
+
} else {
|
|
297
|
+
this._saveQoeKpis(e);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let events = [...filteredFreshEvents];
|
|
303
|
+
let currentPayloadSize = dataSize(filteredFreshEvents);
|
|
249
304
|
|
|
250
305
|
// Always check retry queue if it has data - no flags needed
|
|
251
306
|
if (this.retryQueueHandler && this.retryQueueHandler.getQueueSize() > 0) {
|
|
@@ -413,4 +468,30 @@ export class HarvestScheduler {
|
|
|
413
468
|
triggerFinalHarvest();
|
|
414
469
|
});
|
|
415
470
|
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Checks if QoE KPIs are unchanged since last send.
|
|
474
|
+
* @param {object} event - QoE event to compare
|
|
475
|
+
* @returns {boolean} True if KPIs are identical to last sent
|
|
476
|
+
* @private
|
|
477
|
+
*/
|
|
478
|
+
_qoeKpisUnchanged(event) {
|
|
479
|
+
if (!this._lastSentQoeKpis) return false;
|
|
480
|
+
for (const key of Constants.QOE_KPI_KEYS) {
|
|
481
|
+
if (event[key] !== this._lastSentQoeKpis[key]) return false;
|
|
482
|
+
}
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Saves QoE KPI values after sending.
|
|
488
|
+
* @param {object} event - QoE event that was sent
|
|
489
|
+
* @private
|
|
490
|
+
*/
|
|
491
|
+
_saveQoeKpis(event) {
|
|
492
|
+
this._lastSentQoeKpis = {};
|
|
493
|
+
for (const key of Constants.QOE_KPI_KEYS) {
|
|
494
|
+
this._lastSentQoeKpis[key] = event[key];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
416
497
|
}
|
package/src/recordEvent.js
CHANGED
|
@@ -57,7 +57,7 @@ export function recordEvent(eventType, attributes = {}) {
|
|
|
57
57
|
// Send to video analytics harvester
|
|
58
58
|
const success = videoAnalyticsHarvester.addEvent(eventObject);
|
|
59
59
|
|
|
60
|
-
if(qoeEventObject) {
|
|
60
|
+
if(qoeEventObject && window?.NRVIDEO?.config?.qoeAggregate) {
|
|
61
61
|
const successQoe = videoAnalyticsHarvester.addEvent(qoeEventObject);
|
|
62
62
|
return success && successQoe;
|
|
63
63
|
}
|
|
@@ -10,15 +10,19 @@ const { COLLECTOR } = Constants;
|
|
|
10
10
|
class VideoConfiguration {
|
|
11
11
|
/**
|
|
12
12
|
* Validates and sets the video analytics configuration.
|
|
13
|
-
* @param {object}
|
|
13
|
+
* @param {object} userInfo - User provided configuration
|
|
14
|
+
* @param {object} [config] - Optional configuration object
|
|
14
15
|
* @returns {boolean} True if configuration is valid and set
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
|
-
setConfiguration(userInfo) {
|
|
18
|
+
setConfiguration(userInfo, config) {
|
|
18
19
|
if (!this.validateRequiredFields(userInfo)) {
|
|
19
20
|
return false;
|
|
20
21
|
}
|
|
21
|
-
this.
|
|
22
|
+
if (!this.validateConfigFields(config)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
this.initializeGlobalConfig(userInfo, config);
|
|
22
26
|
Log.notice("Video analytics configuration initialized successfully");
|
|
23
27
|
return true;
|
|
24
28
|
}
|
|
@@ -73,10 +77,49 @@ class VideoConfiguration {
|
|
|
73
77
|
return true;
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Validates optional config fields.
|
|
82
|
+
* @param {object} config - Config to validate
|
|
83
|
+
* @returns {boolean} True if valid
|
|
84
|
+
*/
|
|
85
|
+
validateConfigFields(config) {
|
|
86
|
+
if (config === null || config === undefined) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (typeof config !== "object" || Array.isArray(config)) {
|
|
91
|
+
Log.error("config must be an object");
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { qoeAggregate } = config;
|
|
96
|
+
|
|
97
|
+
if (qoeAggregate !== undefined && typeof qoeAggregate !== "boolean") {
|
|
98
|
+
Log.error("qoeAggregate must be a boolean");
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sanitizes qoeIntervalFactor, defaulting to 1 if the value is not a positive integer.
|
|
107
|
+
* @param {*} value
|
|
108
|
+
* @returns {number}
|
|
109
|
+
*/
|
|
110
|
+
sanitizeQoeIntervalFactor(value) {
|
|
111
|
+
if (value === undefined || value === null) return 1;
|
|
112
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
|
|
113
|
+
console.warn(`[nrvideo] Invalid qoeIntervalFactor "${value}" — must be a positive integer. Defaulting to 1.`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
76
117
|
/**
|
|
77
118
|
* Initializes the global NRVIDEO configuration object.
|
|
119
|
+
* @param {object} userInfo - User provided configuration
|
|
120
|
+
* @param {object} [config] - Optional configuration object
|
|
78
121
|
*/
|
|
79
|
-
initializeGlobalConfig(userInfo) {
|
|
122
|
+
initializeGlobalConfig(userInfo, config) {
|
|
80
123
|
|
|
81
124
|
let { licenseKey, appName, region, beacon, applicationID } = userInfo;
|
|
82
125
|
|
|
@@ -95,6 +138,10 @@ class VideoConfiguration {
|
|
|
95
138
|
applicationID,
|
|
96
139
|
...(applicationID ? {} : { appName }), // Only include appName when no applicationID
|
|
97
140
|
},
|
|
141
|
+
config: {
|
|
142
|
+
qoeAggregate: config?.qoeAggregate ?? false,
|
|
143
|
+
qoeIntervalFactor: this.sanitizeQoeIntervalFactor(config?.qoeIntervalFactor),
|
|
144
|
+
}
|
|
98
145
|
};
|
|
99
146
|
}
|
|
100
147
|
}
|
|
@@ -104,11 +151,12 @@ const videoConfiguration = new VideoConfiguration();
|
|
|
104
151
|
|
|
105
152
|
/**
|
|
106
153
|
* Sets the video analytics configuration.
|
|
107
|
-
* @param {object}
|
|
154
|
+
* @param {object} info - Info configuration object
|
|
155
|
+
* @param {object} [config] - Optional configuration object
|
|
108
156
|
* @returns {boolean} True if configuration was set successfully
|
|
109
157
|
*/
|
|
110
|
-
export function setVideoConfig(info) {
|
|
111
|
-
return videoConfiguration.setConfiguration(info);
|
|
158
|
+
export function setVideoConfig(info, config) {
|
|
159
|
+
return videoConfiguration.setConfiguration(info, config);
|
|
112
160
|
}
|
|
113
161
|
|
|
114
162
|
export { videoConfiguration };
|
package/src/videotracker.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Log from "./log";
|
|
2
2
|
import Tracker from "./tracker";
|
|
3
3
|
import TrackerState from "./videotrackerstate";
|
|
4
|
+
import { videoAnalyticsHarvester } from "./agent";
|
|
4
5
|
import pkg from "../package.json";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -239,6 +240,21 @@ class VideoTracker extends Tracker {
|
|
|
239
240
|
return null;
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
/** Override to return the manifest-declared bitrate in bps (Indicated Bitrate). */
|
|
244
|
+
getManifestBitrate() {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Override to return the measured network bitrate in bps (Observed Bitrate). */
|
|
249
|
+
getMeasuredBitrate() {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Override to return the download throughput in bps. */
|
|
254
|
+
getDownloadBitrate() {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
242
258
|
/** Calculates consumed bitrate using webkitVideoDecodedByteCount. */
|
|
243
259
|
getWebkitBitrate() {
|
|
244
260
|
if (this.tag && this.tag.webkitVideoDecodedByteCount) {
|
|
@@ -465,12 +481,15 @@ class VideoTracker extends Tracker {
|
|
|
465
481
|
att.adTitle = this.getTitle();
|
|
466
482
|
att.adSrc = this.getSrc();
|
|
467
483
|
att.adCdn = this.getCdn();
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
484
|
+
|
|
485
|
+
// Only add bitrate attributes after ad has started
|
|
486
|
+
if (this.state.isStarted) {
|
|
487
|
+
att.adBitrate =
|
|
488
|
+
this.getBitrate() || 0;
|
|
489
|
+
att.adRenditionBitrate = this.getRenditionBitrate() || 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
472
492
|
att.adRenditionName = this.getRenditionName();
|
|
473
|
-
att.adRenditionBitrate = this.getRenditionBitrate();
|
|
474
493
|
att.adRenditionHeight = this.getRenditionHeight();
|
|
475
494
|
att.adRenditionWidth = this.getRenditionWidth();
|
|
476
495
|
att.adDuration = this.getDuration();
|
|
@@ -492,13 +511,17 @@ class VideoTracker extends Tracker {
|
|
|
492
511
|
att.contentPlayhead = this.getPlayhead();
|
|
493
512
|
|
|
494
513
|
att.contentIsLive = this.isLive();
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
this.
|
|
514
|
+
|
|
515
|
+
// Only add bitrate attributes after content has started
|
|
516
|
+
if (this.state.isStarted) {
|
|
517
|
+
att.contentBitrate = this.getBitrate()|| 0;
|
|
518
|
+
att.contentRenditionBitrate = this.getRenditionBitrate() || 0;
|
|
519
|
+
att.contentManifestBitrate = this.getManifestBitrate() || 0;
|
|
520
|
+
att.contentMeasuredBitrate = this.getMeasuredBitrate() || 0;
|
|
521
|
+
att.contentDownloadBitrate = this.getDownloadBitrate() || 0;
|
|
522
|
+
}
|
|
499
523
|
|
|
500
524
|
att.contentRenditionName = this.getRenditionName();
|
|
501
|
-
att.contentRenditionBitrate = this.getRenditionBitrate();
|
|
502
525
|
att.contentRenditionHeight = this.getRenditionHeight();
|
|
503
526
|
att.contentRenditionWidth = this.getRenditionWidth();
|
|
504
527
|
att.contentDuration = this.getDuration();
|
|
@@ -610,15 +633,23 @@ class VideoTracker extends Tracker {
|
|
|
610
633
|
if(this.adsTracker) {
|
|
611
634
|
// If ads state is set to playing (ad error) after content start, reset the ad state.
|
|
612
635
|
if(this.adsTracker.state.isPlaying || this.adsTracker.state.isBuffering) {
|
|
613
|
-
|
|
636
|
+
this.adsTracker.state.stopAdsTime();
|
|
614
637
|
this.adsTracker.state.isPlaying = false;
|
|
615
638
|
this.adsTracker.state.isBuffering = false;
|
|
616
|
-
} else {
|
|
617
|
-
totalAdsTime = this.adsTracker.state.totalAdTime() ?? 0;
|
|
618
639
|
}
|
|
640
|
+
// Always use totalAdTime() which includes accumulator across all ads
|
|
641
|
+
totalAdsTime = this.adsTracker.state.totalAdTime() ?? 0;
|
|
619
642
|
}
|
|
620
643
|
this.state.setStartupTime(totalAdsTime)
|
|
621
644
|
this.sendVideoAction(ev, att);
|
|
645
|
+
|
|
646
|
+
// Register callback to refresh QoE KPIs with latest state before each drain
|
|
647
|
+
videoAnalyticsHarvester.setBeforeDrainCallback(() => {
|
|
648
|
+
if (this.state) {
|
|
649
|
+
const freshKpis = this.state.getQoeAttributes({}).qoe;
|
|
650
|
+
videoAnalyticsHarvester.refreshQoeKpis(freshKpis);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
622
653
|
}
|
|
623
654
|
//this.send(ev, att);
|
|
624
655
|
this.startHeartbeat();
|
|
@@ -658,6 +689,11 @@ class VideoTracker extends Tracker {
|
|
|
658
689
|
this.state.goViewCountUp();
|
|
659
690
|
this.state.totalPlaytime = 0;
|
|
660
691
|
if(!this.isAd()) {
|
|
692
|
+
// Force QoE to be included in the next harvest cycle at content end
|
|
693
|
+
videoAnalyticsHarvester.forceNextQoeCycle();
|
|
694
|
+
// Clear the before-drain callback so the next harvest doesn't overwrite
|
|
695
|
+
// the final QoE (already in buffer) with zeroed-out state values
|
|
696
|
+
videoAnalyticsHarvester.setBeforeDrainCallback(null);
|
|
661
697
|
// reset the states after the view count is up
|
|
662
698
|
if(this.adsTracker) this.adsTracker.state.clearTotalAdsTime();
|
|
663
699
|
this.state.resetViewIdTrackedState();
|
package/src/videotrackerstate.js
CHANGED
|
@@ -104,14 +104,19 @@ class VideoTrackerState {
|
|
|
104
104
|
this.partialAverageBitrate = 0;
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
107
|
+
* Total duration (ms) of all closed bitrate segments for weighted average
|
|
108
108
|
*/
|
|
109
|
-
this.
|
|
109
|
+
this._totalBitrateDuration = 0;
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Had
|
|
112
|
+
* Had Startup Error: TRUE if CONTENT_ERROR occurs before CONTENT_START.
|
|
113
113
|
*/
|
|
114
|
-
this.
|
|
114
|
+
this.hadStartupError = false;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Had Playback Error: TRUE if CONTENT_ERROR occurs during content playback.
|
|
118
|
+
*/
|
|
119
|
+
this.hadPlaybackError = false;
|
|
115
120
|
|
|
116
121
|
/**
|
|
117
122
|
* The amount of ms the user has been rebuffering during content playback.
|
|
@@ -354,8 +359,8 @@ class VideoTrackerState {
|
|
|
354
359
|
if (this.peakBitrate > 0) {
|
|
355
360
|
kpi["peakBitrate"] = this.peakBitrate;
|
|
356
361
|
}
|
|
357
|
-
kpi["
|
|
358
|
-
kpi["
|
|
362
|
+
kpi["hadStartupError"] = this.hadStartupError;
|
|
363
|
+
kpi["hadPlaybackError"] = this.hadPlaybackError;
|
|
359
364
|
kpi["totalRebufferingTime"] = this.totalRebufferingTime;
|
|
360
365
|
// Calculate rebuffering ratio as percentage (avoid division by zero)
|
|
361
366
|
kpi["rebufferingRatio"] = this.totalPlaytime > 0
|
|
@@ -549,8 +554,10 @@ class VideoTrackerState {
|
|
|
549
554
|
}
|
|
550
555
|
|
|
551
556
|
// Accumulate total rebuffering time for content only
|
|
557
|
+
// Use exact stopped duration (stopTime - startTime) instead of live getDeltaTime()
|
|
552
558
|
if (!this.isAd() && this.initialBufferingHappened) {
|
|
553
|
-
this.totalRebufferingTime +=
|
|
559
|
+
this.totalRebufferingTime +=
|
|
560
|
+
(this.timeSinceBufferBegin.stopTime - this.timeSinceBufferBegin.startTime);
|
|
554
561
|
}
|
|
555
562
|
|
|
556
563
|
return true;
|
|
@@ -670,13 +677,13 @@ class VideoTrackerState {
|
|
|
670
677
|
} else {
|
|
671
678
|
this.timeSinceLastError.start();
|
|
672
679
|
|
|
673
|
-
// Track
|
|
674
|
-
// Had Startup
|
|
680
|
+
// Track error flags for content errors only
|
|
681
|
+
// Had Startup Error: error before content started
|
|
675
682
|
if (!this.isStarted) {
|
|
676
|
-
this.
|
|
683
|
+
this.hadStartupError = true;
|
|
677
684
|
} else {
|
|
678
|
-
// Had Playback
|
|
679
|
-
this.
|
|
685
|
+
// Had Playback Error: any content error during playback
|
|
686
|
+
this.hadPlaybackError = true;
|
|
680
687
|
}
|
|
681
688
|
}
|
|
682
689
|
}
|
|
@@ -696,14 +703,57 @@ class VideoTrackerState {
|
|
|
696
703
|
if (bitrate && typeof bitrate === "number") {
|
|
697
704
|
this.peakBitrate = Math.max(this.peakBitrate, bitrate);
|
|
698
705
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
706
|
+
const now = Date.now();
|
|
707
|
+
|
|
708
|
+
// If not playing (buffering, paused, etc.), close the current segment
|
|
709
|
+
// so non-play time is excluded from the weighted average
|
|
710
|
+
if (!this.isPlaying) {
|
|
711
|
+
if (this._lastBitrate !== null && this._lastBitrateChangeTimestamp !== null) {
|
|
712
|
+
const segmentDuration = now - this._lastBitrateChangeTimestamp;
|
|
713
|
+
if (segmentDuration > 0) {
|
|
714
|
+
this.partialAverageBitrate += this._lastBitrate * segmentDuration;
|
|
715
|
+
this._totalBitrateDuration += segmentDuration;
|
|
716
|
+
}
|
|
717
|
+
this._lastBitrateChangeTimestamp = null; // Mark as paused
|
|
718
|
+
}
|
|
704
719
|
this._lastBitrate = bitrate;
|
|
705
|
-
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Playing: restart timestamp if returning from non-play state
|
|
724
|
+
if (this._lastBitrateChangeTimestamp === null && this._lastBitrate !== null) {
|
|
725
|
+
this._lastBitrateChangeTimestamp = now;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Close the PREVIOUS segment when bitrate changes
|
|
729
|
+
if (this._lastBitrate !== null && this._lastBitrate !== bitrate
|
|
730
|
+
&& this._lastBitrateChangeTimestamp !== null) {
|
|
731
|
+
const segmentDuration = now - this._lastBitrateChangeTimestamp;
|
|
732
|
+
if (segmentDuration > 0) {
|
|
733
|
+
this.partialAverageBitrate += this._lastBitrate * segmentDuration;
|
|
734
|
+
this._totalBitrateDuration += segmentDuration;
|
|
735
|
+
}
|
|
736
|
+
this._lastBitrateChangeTimestamp = now;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Initialize timestamp on first observation
|
|
740
|
+
if (this._lastBitrateChangeTimestamp === null) {
|
|
741
|
+
this._lastBitrateChangeTimestamp = now;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
this._lastBitrate = bitrate;
|
|
745
|
+
|
|
746
|
+
// Compute weighted average including current in-progress segment
|
|
747
|
+
let totalWeighted = this.partialAverageBitrate;
|
|
748
|
+
let totalDuration = this._totalBitrateDuration;
|
|
749
|
+
const currentSegment = now - this._lastBitrateChangeTimestamp;
|
|
750
|
+
if (currentSegment > 0) {
|
|
751
|
+
totalWeighted += bitrate * currentSegment;
|
|
752
|
+
totalDuration += currentSegment;
|
|
706
753
|
}
|
|
754
|
+
this.weightedBitrate = totalDuration > 0
|
|
755
|
+
? Math.round(totalWeighted / totalDuration)
|
|
756
|
+
: bitrate;
|
|
707
757
|
}
|
|
708
758
|
}
|
|
709
759
|
|
|
@@ -713,6 +763,7 @@ class VideoTrackerState {
|
|
|
713
763
|
resetViewIdTrackedState() {
|
|
714
764
|
this.peakBitrate = 0;
|
|
715
765
|
this.partialAverageBitrate = 0;
|
|
766
|
+
this._totalBitrateDuration = 0;
|
|
716
767
|
this.startupTime = null;
|
|
717
768
|
this._lastBitrate = null;
|
|
718
769
|
this._lastBitrateChangeTimestamp = null;
|
|
@@ -720,7 +771,7 @@ class VideoTrackerState {
|
|
|
720
771
|
|
|
721
772
|
/** Methods to manage total ads time chrono */
|
|
722
773
|
clearTotalAdsTime() {
|
|
723
|
-
|
|
774
|
+
Log.debug("clear total ads time", this.totalAdTime);
|
|
724
775
|
this._totalAdPlaytime.reset();
|
|
725
776
|
}
|
|
726
777
|
|
|
@@ -729,12 +780,12 @@ class VideoTrackerState {
|
|
|
729
780
|
}
|
|
730
781
|
|
|
731
782
|
startAdsTime() {
|
|
732
|
-
|
|
783
|
+
Log.debug("startAdsTime");
|
|
733
784
|
return this._totalAdPlaytime.start();
|
|
734
785
|
}
|
|
735
786
|
|
|
736
787
|
stopAdsTime() {
|
|
737
|
-
|
|
788
|
+
Log.debug("stopAdsTime");
|
|
738
789
|
return this._totalAdPlaytime.stop();
|
|
739
790
|
}
|
|
740
791
|
|