@newrelic/video-core 4.1.8 → 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/README.md +140 -27
- 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 +25 -7
- package/src/{agent.js → browser/agent.js} +14 -35
- package/src/{harvestScheduler.js → browser/harvestScheduler.js} +55 -137
- 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 +2 -0
- package/src/core.js +4 -18
- package/src/index.js +9 -10
- 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 +56 -10
- package/src/videotracker.js +26 -9
package/src/constants.js
CHANGED
|
@@ -25,6 +25,7 @@ Constants.AdPositions = {
|
|
|
25
25
|
Constants.COLLECTOR = {
|
|
26
26
|
US: ["bam.nr-data.net", "bam-cell.nr-data.net"],
|
|
27
27
|
EU: "bam.eu01.nr-data.net",
|
|
28
|
+
JP: "bam.jp.nr-data.net",
|
|
28
29
|
Staging: "staging-bam-cell.nr-data.net",
|
|
29
30
|
GOV: "gov-bam.nr-data.net",
|
|
30
31
|
};
|
|
@@ -41,6 +42,7 @@ Constants.MAX_PAYLOAD_SIZE = 1048576; // 1MB = 1024 × 1024 bytes
|
|
|
41
42
|
Constants.MAX_BEACON_SIZE = 61440; // 60KB = 60 × 1024 bytes
|
|
42
43
|
Constants.MAX_EVENTS_PER_BATCH = 1000;
|
|
43
44
|
Constants.INTERVAL = 10000; //10 seconds
|
|
45
|
+
Constants.DEFAULT_QOE_INTERVAL_FACTOR = 2;
|
|
44
46
|
|
|
45
47
|
Constants.QOE_KPI_KEYS = [
|
|
46
48
|
"startupTime", "peakBitrate", "averageBitrate", "totalPlaytime",
|
package/src/core.js
CHANGED
|
@@ -14,9 +14,11 @@ class Core {
|
|
|
14
14
|
* @param {object} options Configuration options including video analytics settings.
|
|
15
15
|
*/
|
|
16
16
|
static addTracker(tracker, options) {
|
|
17
|
-
// Set video analytics configuration
|
|
17
|
+
// Set video analytics configuration. The optional `options.src` field
|
|
18
|
+
// selects the pipeline: `'Vega'` writes globalThis.__NRVIDEO_CD__,
|
|
19
|
+
// anything else writes window.NRVIDEO.
|
|
18
20
|
if (options?.info) {
|
|
19
|
-
setVideoConfig(options.info, options?.config);
|
|
21
|
+
setVideoConfig(options.info, options?.config, options?.src);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
if (tracker.on && tracker.emit) {
|
|
@@ -82,22 +84,6 @@ class Core {
|
|
|
82
84
|
});
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Forces an immediate harvest of all pending events.
|
|
90
|
-
* @returns {Promise<object>} Harvest result
|
|
91
|
-
*/
|
|
92
|
-
static async forceHarvest() {
|
|
93
|
-
try {
|
|
94
|
-
const { videoAnalyticsHarvester } = require("./agent"); // lazy loading for dynamic import
|
|
95
|
-
return await videoAnalyticsHarvester.forceHarvest();
|
|
96
|
-
} catch (error) {
|
|
97
|
-
Log.error("Failed to force harvest:", error.message);
|
|
98
|
-
return { success: false, error: error.message };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
87
|
}
|
|
102
88
|
|
|
103
89
|
let trackers = [];
|
package/src/index.js
CHANGED
|
@@ -9,12 +9,14 @@ import VideoTrackerState from "./videotrackerstate";
|
|
|
9
9
|
import { NrVideoEventAggregator } from "./eventAggregator";
|
|
10
10
|
import { RetryQueueHandler } from "./retryQueueHandler";
|
|
11
11
|
import { OptimizedHttpClient } from "./optimizedHttpClient";
|
|
12
|
-
import { HarvestScheduler } from "./harvestScheduler";
|
|
13
|
-
import { recordEvent } from "./recordEvent";
|
|
12
|
+
import { HarvestScheduler } from "./browser/harvestScheduler";
|
|
13
|
+
import { recordEvent, getRegisteredHarvester } from "./recordEvent";
|
|
14
14
|
import { version } from "../package.json";
|
|
15
15
|
|
|
16
|
+
// Harvesters are exported as NAMED exports only — never added to the `nrvideo`
|
|
17
|
+
// default-namespace object. This is the load-bearing detail for tree-shaking
|
|
18
|
+
|
|
16
19
|
const nrvideo = {
|
|
17
|
-
// Core components (existing)
|
|
18
20
|
Constants,
|
|
19
21
|
Chrono,
|
|
20
22
|
Log,
|
|
@@ -25,19 +27,16 @@ const nrvideo = {
|
|
|
25
27
|
Core,
|
|
26
28
|
version,
|
|
27
29
|
|
|
28
|
-
// Enhanced video analytics components (new)
|
|
29
|
-
|
|
30
30
|
NrVideoEventAggregator,
|
|
31
31
|
RetryQueueHandler,
|
|
32
32
|
OptimizedHttpClient,
|
|
33
33
|
HarvestScheduler,
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Enhanced event recording
|
|
38
35
|
recordEvent,
|
|
39
|
-
|
|
40
|
-
|
|
41
36
|
};
|
|
42
37
|
|
|
38
|
+
export { videoAnalyticsHarvester } from "./browser/agent";
|
|
39
|
+
export { connectedDeviceAnalyticsHarvester } from "./connectedDevice/connectedDeviceAgent";
|
|
40
|
+
export { getRegisteredHarvester } from "./recordEvent";
|
|
41
|
+
|
|
43
42
|
export default nrvideo;
|
package/src/recordEvent.js
CHANGED
|
@@ -1,68 +1,75 @@
|
|
|
1
|
-
import { videoAnalyticsHarvester } from "./agent.js";
|
|
2
1
|
import Constants from "./constants.js";
|
|
3
2
|
import Log from "./log.js";
|
|
4
|
-
import
|
|
5
|
-
import {getObjectEntriesForKeys} from "./utils";
|
|
3
|
+
import { dispatchRecordEvent } from "./utils/eventBuilder";
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Registry-based recordEvent (split build).
|
|
7
|
+
*
|
|
8
|
+
* No static harvester imports. Each harvester self-registers under a key
|
|
9
|
+
* (`'Browser'` or `'Vega'`) at module load time via `registerHarvester`.
|
|
11
10
|
*/
|
|
11
|
+
const harvesters = Object.create(null);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register a harvester implementation under a routing key.
|
|
15
|
+
* Called by `agent.js` (key `'Browser'`) and `connectedDeviceAgent.js`
|
|
16
|
+
* (key `'Vega'`) at module load time.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} key Routing key matching `attributes.src`.
|
|
19
|
+
* @param {{ addEvent: function }} harvester
|
|
20
|
+
*/
|
|
21
|
+
export function registerHarvester(key, harvester) {
|
|
22
|
+
harvesters[key] = harvester;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Look up a harvester registered under a routing key. Returns `undefined` if
|
|
27
|
+
* no module has registered for that key in this build (e.g. on the `/browser`
|
|
28
|
+
* subpath, the 'Vega' key is never registered because `connectedDeviceAgent.js`
|
|
29
|
+
* is unreachable from that entry's import graph).
|
|
30
|
+
*
|
|
31
|
+
* Trackers use this getter — instead of importing the harvester binding by
|
|
32
|
+
* name — so the same `tracker.js` / `vegaTracker.js` source files compile
|
|
33
|
+
* unchanged across all three core entry points (main, /browser, /vega) when
|
|
34
|
+
* the html5 webpack build aliases `@newrelic/video-core` to a specific
|
|
35
|
+
* subpath. Without this, parent-class file `tracker.js` would carry an
|
|
36
|
+
* unconditional `import { videoAnalyticsHarvester }` that fails to resolve
|
|
37
|
+
* against the `/vega` subpath.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} key
|
|
40
|
+
* @returns {{ addEvent: function }|undefined}
|
|
41
|
+
*/
|
|
42
|
+
export function getRegisteredHarvester(key) {
|
|
43
|
+
return harvesters[key];
|
|
44
|
+
}
|
|
45
|
+
|
|
12
46
|
export function recordEvent(eventType, attributes = {}) {
|
|
13
47
|
try {
|
|
14
|
-
// Validate event type
|
|
15
48
|
if (!Constants.VALID_EVENT_TYPES.includes(eventType)) {
|
|
16
49
|
Log.warn("Invalid event type provided to recordEvent", { eventType });
|
|
17
50
|
return false;
|
|
18
51
|
}
|
|
19
52
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
53
|
+
const isVega = attributes.src === "Vega";
|
|
54
|
+
const routingKey = isVega ? "Vega" : "Browser";
|
|
55
|
+
const w = typeof window !== "undefined" ? window : undefined;
|
|
56
|
+
const info = isVega ? globalThis.__NRVIDEO_CD__?.info : w?.NRVIDEO?.info;
|
|
57
|
+
if (!info) return;
|
|
23
58
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const otherAttrs = {
|
|
30
|
-
...(applicationID ? {} : { appName }), // Only include appName when no applicationID
|
|
31
|
-
timestamp: Date.now(),
|
|
32
|
-
timeSinceLoad: window.performance
|
|
33
|
-
? window.performance.now() / 1000
|
|
34
|
-
: null,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const eventObject = {
|
|
38
|
-
...eventAttributes,
|
|
39
|
-
eventType,
|
|
40
|
-
...otherAttrs,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const metadataAttributes = getObjectEntriesForKeys(Constants.QOE_AGGREGATE_KEYS, attributes)
|
|
44
|
-
|
|
45
|
-
let qoeEventObject = null;
|
|
46
|
-
if(eventType === "VideoAction") {
|
|
47
|
-
qoeEventObject = {
|
|
48
|
-
eventType: "VideoAction",
|
|
49
|
-
actionName: Tracker.Events.QOE_AGGREGATE,
|
|
50
|
-
qoeAggregateVersion: '1.0.0',
|
|
51
|
-
...qoeAttrs,
|
|
52
|
-
...metadataAttributes,
|
|
53
|
-
...otherAttrs,
|
|
54
|
-
}
|
|
59
|
+
const harvester = harvesters[routingKey];
|
|
60
|
+
if (!harvester) {
|
|
61
|
+
Log.warn("No harvester registered for routing key", { routingKey });
|
|
62
|
+
return false;
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if(qoeEventObject && window?.NRVIDEO?.config?.qoeAggregate) {
|
|
61
|
-
const successQoe = videoAnalyticsHarvester.addEvent(qoeEventObject);
|
|
62
|
-
return success && successQoe;
|
|
63
|
-
}
|
|
65
|
+
const qoeEnabled = isVega
|
|
66
|
+
? globalThis.__NRVIDEO_CD__?.config?.qoeAggregate
|
|
67
|
+
: w?.NRVIDEO?.config?.qoeAggregate;
|
|
64
68
|
|
|
65
|
-
return
|
|
69
|
+
return dispatchRecordEvent(
|
|
70
|
+
eventType, attributes, info, harvester, qoeEnabled,
|
|
71
|
+
{ addTimeSinceLoad: !isVega }
|
|
72
|
+
);
|
|
66
73
|
} catch (error) {
|
|
67
74
|
Log.error("Failed to record event:", error.message);
|
|
68
75
|
return false;
|
package/src/tracker.js
CHANGED
|
@@ -2,7 +2,6 @@ import pkg from "../package.json";
|
|
|
2
2
|
import Emitter from "./emitter";
|
|
3
3
|
import Chrono from "./chrono";
|
|
4
4
|
import Constants from "./constants";
|
|
5
|
-
import { videoAnalyticsHarvester } from "./agent";
|
|
6
5
|
import Log from "./log";
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -269,18 +268,29 @@ class Tracker extends Emitter {
|
|
|
269
268
|
*/
|
|
270
269
|
|
|
271
270
|
setHarvestInterval(interval) {
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
const harvester = this.getHarvester?.();
|
|
272
|
+
if (!harvester) {
|
|
273
|
+
Log.error("Tracker has no harvester; setHarvestInterval ignored");
|
|
274
274
|
return;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
try {
|
|
278
|
-
|
|
278
|
+
harvester.setHarvestInterval(interval);
|
|
279
279
|
} catch (error) {
|
|
280
280
|
Log.error("Failed to set harvest interval:", error.message);
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Default harvester accessor. Subclasses (Html5Tracker, VegaTracker, etc.)
|
|
287
|
+
* override this to point to their specific harvester. Returning `null` here
|
|
288
|
+
* means a tracker that doesn't override `getHarvester()` will silently no-op
|
|
289
|
+
* on harvester-bound calls (setHarvestInterval, QoE drain wiring).
|
|
290
|
+
*/
|
|
291
|
+
getHarvester() {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
284
294
|
}
|
|
285
295
|
|
|
286
296
|
/**
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import Constants from "../constants";
|
|
2
|
+
import Tracker from "../tracker";
|
|
3
|
+
import { getObjectEntriesForKeys } from "./index";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pure helper that constructs the wire-format event objects from `recordEvent`
|
|
7
|
+
* arguments. Used by `src/recordEvent.js`, which is the single shared
|
|
8
|
+
* implementation re-exported from all three subpath entry points:
|
|
9
|
+
*
|
|
10
|
+
* - `src/recordEvent.js` (registry dispatcher; runtime hot path
|
|
11
|
+
* and the only direct caller of this helper)
|
|
12
|
+
* - `src/browser/index.js` (Browser subpath re-export)
|
|
13
|
+
* - `src/connectedDevice/index.js` (Vega subpath re-export)
|
|
14
|
+
*
|
|
15
|
+
* Pipeline-specific concerns (which info global to read, which harvester to
|
|
16
|
+
* call, which qoeAggregate config to check) stay at the call site. This helper
|
|
17
|
+
* is purely about event-object shape.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} eventType - One of `Constants.VALID_EVENT_TYPES`.
|
|
20
|
+
* @param {object} attributes - Caller-supplied attributes including optional `qoe`.
|
|
21
|
+
* @param {object} info - The pipeline's resolved `info` object (with `appName` /
|
|
22
|
+
* `applicationID` for namespacing).
|
|
23
|
+
* @param {{ addTimeSinceLoad?: boolean }} [opts] - When `true`, stamp
|
|
24
|
+
* `timeSinceLoad` on the event. Browser sets this; Vega does not.
|
|
25
|
+
*
|
|
26
|
+
* @returns {{ eventObject: object, qoeEventObject: object|null }}
|
|
27
|
+
*/
|
|
28
|
+
export function buildEventObjects(
|
|
29
|
+
eventType,
|
|
30
|
+
attributes,
|
|
31
|
+
info,
|
|
32
|
+
{ addTimeSinceLoad = false } = {}
|
|
33
|
+
) {
|
|
34
|
+
const { appName, applicationID } = info;
|
|
35
|
+
const { qoe, ...eventAttributes } = attributes;
|
|
36
|
+
const qoeAttrs = qoe ? { ...qoe } : {};
|
|
37
|
+
|
|
38
|
+
const otherAttrs = {
|
|
39
|
+
...(applicationID ? {} : { appName }),
|
|
40
|
+
timestamp: Date.now(),
|
|
41
|
+
...(addTimeSinceLoad
|
|
42
|
+
? {
|
|
43
|
+
timeSinceLoad:
|
|
44
|
+
typeof window !== "undefined" && window.performance
|
|
45
|
+
? window.performance.now() / 1000
|
|
46
|
+
: null,
|
|
47
|
+
}
|
|
48
|
+
: {}),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const eventObject = {
|
|
52
|
+
...eventAttributes,
|
|
53
|
+
eventType,
|
|
54
|
+
...otherAttrs,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let qoeEventObject = null;
|
|
58
|
+
if (eventType === "VideoAction") {
|
|
59
|
+
const metadataAttributes = getObjectEntriesForKeys(
|
|
60
|
+
Constants.QOE_AGGREGATE_KEYS,
|
|
61
|
+
attributes
|
|
62
|
+
);
|
|
63
|
+
qoeEventObject = {
|
|
64
|
+
eventType: "VideoAction",
|
|
65
|
+
actionName: Tracker.Events.QOE_AGGREGATE,
|
|
66
|
+
qoeAggregateVersion: "1.0.0",
|
|
67
|
+
...qoeAttrs,
|
|
68
|
+
...metadataAttributes,
|
|
69
|
+
...otherAttrs,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { eventObject, qoeEventObject };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build + dispatch a video analytics event to the given harvester.
|
|
78
|
+
*
|
|
79
|
+
* This is the full dispatch loop shared by all three `recordEvent` entry
|
|
80
|
+
* points:
|
|
81
|
+
* 1. Build `eventObject` (and optional `qoeEventObject`) via
|
|
82
|
+
* `buildEventObjects`.
|
|
83
|
+
* 2. Call `harvester.addEvent(eventObject)`.
|
|
84
|
+
* 3. If qoeEnabled and a QoE companion event was produced, also call
|
|
85
|
+
* `harvester.addEvent(qoeEventObject)`.
|
|
86
|
+
* 4. Return a boolean indicating overall success.
|
|
87
|
+
*
|
|
88
|
+
* Pipeline-specific concerns — which global to read for `info`, which
|
|
89
|
+
* harvester to use, whether qoeAggregate is on — are resolved by the caller.
|
|
90
|
+
* This function only handles the build + dispatch.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} eventType
|
|
93
|
+
* @param {object} attributes
|
|
94
|
+
* @param {object} info - Resolved `info` from the pipeline's config global.
|
|
95
|
+
* @param {{ addEvent: function }} harvester - The registered harvester for
|
|
96
|
+
* the current pipeline. Returns false if null/undefined.
|
|
97
|
+
* @param {boolean} qoeEnabled
|
|
98
|
+
* @param {{ addTimeSinceLoad?: boolean }} [opts] - Forwarded to
|
|
99
|
+
* `buildEventObjects`.
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
export function dispatchRecordEvent(
|
|
103
|
+
eventType,
|
|
104
|
+
attributes,
|
|
105
|
+
info,
|
|
106
|
+
harvester,
|
|
107
|
+
qoeEnabled,
|
|
108
|
+
{ addTimeSinceLoad = false } = {}
|
|
109
|
+
) {
|
|
110
|
+
if (!harvester) return false;
|
|
111
|
+
|
|
112
|
+
const { eventObject, qoeEventObject } = buildEventObjects(
|
|
113
|
+
eventType,
|
|
114
|
+
attributes,
|
|
115
|
+
info,
|
|
116
|
+
{ addTimeSinceLoad }
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const success = harvester.addEvent(eventObject);
|
|
120
|
+
|
|
121
|
+
if (qoeEventObject && qoeEnabled) {
|
|
122
|
+
return success && harvester.addEvent(qoeEventObject);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return success;
|
|
126
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import Log from "../log";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Periodic harvest timer using chained `setTimeout`. Each tick schedules the
|
|
5
|
+
* next one only after the current `onTick` callback resolves — by construction
|
|
6
|
+
* there can never be overlapping ticks, even if `onTick` is async and slow.
|
|
7
|
+
*
|
|
8
|
+
* Used by both pipelines:
|
|
9
|
+
* - `browser/harvestScheduler.js` — Browser pipeline drain orchestration
|
|
10
|
+
* - `connectedDevice/connectedDeviceHarvester.js` — Vega CAF pipeline
|
|
11
|
+
*
|
|
12
|
+
* Single source of truth for harvest timing. Future timer changes (drift
|
|
13
|
+
* compensation, jitter, exponential back-off, etc.) land here and apply to
|
|
14
|
+
* both pipelines automatically.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} opts
|
|
17
|
+
* @param {number} opts.interval - Initial tick interval in ms.
|
|
18
|
+
* @param {() => Promise<void>|void} opts.onTick - Pipeline-specific drain
|
|
19
|
+
* callback. Awaited; the next tick is scheduled in `finally` so a slow or
|
|
20
|
+
* throwing tick can't overlap with the next one.
|
|
21
|
+
* @param {string} opts.errorLabel - Prefix for the error log when `onTick`
|
|
22
|
+
* throws (e.g. "HarvestScheduler", "ConnectedDeviceHarvester").
|
|
23
|
+
*
|
|
24
|
+
* @returns {{
|
|
25
|
+
* start: () => void,
|
|
26
|
+
* stop: () => void,
|
|
27
|
+
* cancelAndReschedule: () => void,
|
|
28
|
+
* updateInterval: (ms: number) => void,
|
|
29
|
+
* isRunning: () => boolean,
|
|
30
|
+
* }}
|
|
31
|
+
*/
|
|
32
|
+
export function createHarvestTimer({ interval, onTick, errorLabel }) {
|
|
33
|
+
let isStarted = false;
|
|
34
|
+
let currentTimerId = null;
|
|
35
|
+
let currentInterval = interval;
|
|
36
|
+
|
|
37
|
+
function scheduleNext() {
|
|
38
|
+
if (!isStarted) return;
|
|
39
|
+
currentTimerId = setTimeout(async () => {
|
|
40
|
+
currentTimerId = null;
|
|
41
|
+
try {
|
|
42
|
+
await onTick();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
Log.error(`${errorLabel}: scheduled tick failed:`, error.message);
|
|
45
|
+
} finally {
|
|
46
|
+
if (isStarted) scheduleNext();
|
|
47
|
+
}
|
|
48
|
+
}, currentInterval);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
/**
|
|
53
|
+
* Start the timer. Idempotent — repeated calls have no effect while
|
|
54
|
+
* already running.
|
|
55
|
+
*/
|
|
56
|
+
start() {
|
|
57
|
+
if (isStarted) return;
|
|
58
|
+
isStarted = true;
|
|
59
|
+
scheduleNext();
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Stop the timer. The pending tick (if any) is cancelled. The currently-
|
|
64
|
+
* executing tick (if any) will see `isStarted === false` in its `finally`
|
|
65
|
+
* and won't schedule a successor.
|
|
66
|
+
*/
|
|
67
|
+
stop() {
|
|
68
|
+
isStarted = false;
|
|
69
|
+
if (currentTimerId) {
|
|
70
|
+
clearTimeout(currentTimerId);
|
|
71
|
+
currentTimerId = null;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Cancel any pending tick and reschedule from now. Used by smart-harvest /
|
|
77
|
+
* forceHarvest paths that drain the buffer immediately and want the
|
|
78
|
+
* periodic clock reset relative to that drain.
|
|
79
|
+
*/
|
|
80
|
+
cancelAndReschedule() {
|
|
81
|
+
if (currentTimerId) {
|
|
82
|
+
clearTimeout(currentTimerId);
|
|
83
|
+
currentTimerId = null;
|
|
84
|
+
}
|
|
85
|
+
if (isStarted) scheduleNext();
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update the tick interval. Takes effect on the next scheduled tick.
|
|
90
|
+
* If the timer is currently running, the pending tick is cancelled and
|
|
91
|
+
* a new one is scheduled with the new interval.
|
|
92
|
+
*
|
|
93
|
+
* @param {number} ms - New interval in milliseconds. Must be a positive
|
|
94
|
+
* number; invalid values are silently ignored.
|
|
95
|
+
*/
|
|
96
|
+
updateInterval(ms) {
|
|
97
|
+
if (typeof ms !== "number" || ms <= 0) return;
|
|
98
|
+
currentInterval = ms;
|
|
99
|
+
if (isStarted && currentTimerId) {
|
|
100
|
+
clearTimeout(currentTimerId);
|
|
101
|
+
currentTimerId = null;
|
|
102
|
+
scheduleNext();
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/** @returns {boolean} True iff `start()` has been called and `stop()` has not. */
|
|
107
|
+
isRunning: () => isStarted,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -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
|
+
}
|