@scarlett-player/analytics 0.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/LICENSE +21 -0
- package/README.md +473 -0
- package/dist/index.cjs +609 -0
- package/dist/index.d.cts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +582 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createAnalyticsPlugin: () => createAnalyticsPlugin,
|
|
24
|
+
default: () => index_default
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/helpers.ts
|
|
29
|
+
function generateId() {
|
|
30
|
+
const timestamp = Date.now();
|
|
31
|
+
const random = Math.random().toString(36).substring(2, 11);
|
|
32
|
+
return `${timestamp}-${random}`;
|
|
33
|
+
}
|
|
34
|
+
function getSessionId() {
|
|
35
|
+
const STORAGE_KEY = "sp_session_id";
|
|
36
|
+
try {
|
|
37
|
+
let sessionId = sessionStorage.getItem(STORAGE_KEY);
|
|
38
|
+
if (!sessionId) {
|
|
39
|
+
sessionId = generateId();
|
|
40
|
+
sessionStorage.setItem(STORAGE_KEY, sessionId);
|
|
41
|
+
}
|
|
42
|
+
return sessionId;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return generateId();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function getAnonymousViewerId() {
|
|
48
|
+
const STORAGE_KEY = "sp_viewer_id";
|
|
49
|
+
try {
|
|
50
|
+
let viewerId = localStorage.getItem(STORAGE_KEY);
|
|
51
|
+
if (!viewerId) {
|
|
52
|
+
viewerId = generateId();
|
|
53
|
+
localStorage.setItem(STORAGE_KEY, viewerId);
|
|
54
|
+
}
|
|
55
|
+
return viewerId;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
try {
|
|
58
|
+
let viewerId = sessionStorage.getItem(STORAGE_KEY);
|
|
59
|
+
if (!viewerId) {
|
|
60
|
+
viewerId = generateId();
|
|
61
|
+
sessionStorage.setItem(STORAGE_KEY, viewerId);
|
|
62
|
+
}
|
|
63
|
+
return viewerId;
|
|
64
|
+
} catch {
|
|
65
|
+
return generateId();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function getBrowserInfo() {
|
|
70
|
+
const ua = navigator.userAgent;
|
|
71
|
+
if (ua.includes("Edg/")) {
|
|
72
|
+
const match = ua.match(/Edg\/(\d+)/);
|
|
73
|
+
return {
|
|
74
|
+
name: "Edge",
|
|
75
|
+
version: match ? match[1] : void 0
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (ua.includes("Chrome/") && !ua.includes("Edg/")) {
|
|
79
|
+
const match = ua.match(/Chrome\/(\d+)/);
|
|
80
|
+
return {
|
|
81
|
+
name: "Chrome",
|
|
82
|
+
version: match ? match[1] : void 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (ua.includes("Safari/") && !ua.includes("Chrome")) {
|
|
86
|
+
const match = ua.match(/Version\/(\d+)/);
|
|
87
|
+
return {
|
|
88
|
+
name: "Safari",
|
|
89
|
+
version: match ? match[1] : void 0
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (ua.includes("Firefox/")) {
|
|
93
|
+
const match = ua.match(/Firefox\/(\d+)/);
|
|
94
|
+
return {
|
|
95
|
+
name: "Firefox",
|
|
96
|
+
version: match ? match[1] : void 0
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (ua.includes("OPR/") || ua.includes("Opera/")) {
|
|
100
|
+
const match = ua.match(/(?:OPR|Opera)\/(\d+)/);
|
|
101
|
+
return {
|
|
102
|
+
name: "Opera",
|
|
103
|
+
version: match ? match[1] : void 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { name: "Unknown" };
|
|
107
|
+
}
|
|
108
|
+
function getOSInfo() {
|
|
109
|
+
const ua = navigator.userAgent;
|
|
110
|
+
const platform = navigator.platform || "";
|
|
111
|
+
if (ua.includes("Windows")) {
|
|
112
|
+
if (ua.includes("Windows NT 10.0")) return { name: "Windows", version: "10" };
|
|
113
|
+
if (ua.includes("Windows NT 6.3")) return { name: "Windows", version: "8.1" };
|
|
114
|
+
if (ua.includes("Windows NT 6.2")) return { name: "Windows", version: "8" };
|
|
115
|
+
if (ua.includes("Windows NT 6.1")) return { name: "Windows", version: "7" };
|
|
116
|
+
return { name: "Windows" };
|
|
117
|
+
}
|
|
118
|
+
if (ua.includes("Mac OS X")) {
|
|
119
|
+
const match = ua.match(/Mac OS X (\d+)[._](\d+)/);
|
|
120
|
+
return {
|
|
121
|
+
name: "macOS",
|
|
122
|
+
version: match ? `${match[1]}.${match[2]}` : void 0
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (ua.includes("iPhone") || ua.includes("iPad") || ua.includes("iPod")) {
|
|
126
|
+
const match = ua.match(/OS (\d+)[._](\d+)/);
|
|
127
|
+
return {
|
|
128
|
+
name: "iOS",
|
|
129
|
+
version: match ? `${match[1]}.${match[2]}` : void 0
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (ua.includes("Android")) {
|
|
133
|
+
const match = ua.match(/Android (\d+(?:\.\d+)?)/);
|
|
134
|
+
return {
|
|
135
|
+
name: "Android",
|
|
136
|
+
version: match ? match[1] : void 0
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (ua.includes("Linux") || platform.includes("Linux")) {
|
|
140
|
+
return { name: "Linux" };
|
|
141
|
+
}
|
|
142
|
+
if (ua.includes("CrOS")) {
|
|
143
|
+
return { name: "ChromeOS" };
|
|
144
|
+
}
|
|
145
|
+
return { name: "Unknown" };
|
|
146
|
+
}
|
|
147
|
+
function getDeviceType() {
|
|
148
|
+
const ua = navigator.userAgent;
|
|
149
|
+
if (ua.includes("TV") || ua.includes("PlayStation") || ua.includes("Xbox") || ua.includes("SmartTV")) {
|
|
150
|
+
return "tv";
|
|
151
|
+
}
|
|
152
|
+
if (ua.includes("iPad") || ua.includes("Android") && !ua.includes("Mobile") || ua.includes("Tablet")) {
|
|
153
|
+
return "tablet";
|
|
154
|
+
}
|
|
155
|
+
if (ua.includes("Mobile") || ua.includes("iPhone") || ua.includes("iPod") || ua.includes("Android") && ua.includes("Mobile")) {
|
|
156
|
+
return "mobile";
|
|
157
|
+
}
|
|
158
|
+
return "desktop";
|
|
159
|
+
}
|
|
160
|
+
function getScreenSize() {
|
|
161
|
+
return `${window.screen.width}x${window.screen.height}`;
|
|
162
|
+
}
|
|
163
|
+
function getPlayerSize(container) {
|
|
164
|
+
if (!container) {
|
|
165
|
+
return `${window.innerWidth}x${window.innerHeight}`;
|
|
166
|
+
}
|
|
167
|
+
const rect = container.getBoundingClientRect();
|
|
168
|
+
return `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
|
169
|
+
}
|
|
170
|
+
function getConnectionType() {
|
|
171
|
+
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
172
|
+
if (conn) {
|
|
173
|
+
return conn.effectiveType || conn.type || "unknown";
|
|
174
|
+
}
|
|
175
|
+
return "unknown";
|
|
176
|
+
}
|
|
177
|
+
function calculateQoEScore(params) {
|
|
178
|
+
const {
|
|
179
|
+
startupTime,
|
|
180
|
+
rebufferDuration,
|
|
181
|
+
watchTime,
|
|
182
|
+
maxBitrate,
|
|
183
|
+
exitType,
|
|
184
|
+
errorCount
|
|
185
|
+
} = params;
|
|
186
|
+
let startupScore = 100;
|
|
187
|
+
if (startupTime !== null) {
|
|
188
|
+
if (startupTime < 1e3) startupScore = 100;
|
|
189
|
+
else if (startupTime < 2e3) startupScore = 85;
|
|
190
|
+
else if (startupTime < 4e3) startupScore = 70;
|
|
191
|
+
else if (startupTime < 8e3) startupScore = 50;
|
|
192
|
+
else startupScore = 30;
|
|
193
|
+
}
|
|
194
|
+
let smoothnessScore = 100;
|
|
195
|
+
if (watchTime > 0) {
|
|
196
|
+
const rebufferRatio = rebufferDuration / watchTime * 100;
|
|
197
|
+
if (rebufferRatio < 0.1) smoothnessScore = 100;
|
|
198
|
+
else if (rebufferRatio < 1) smoothnessScore = 85;
|
|
199
|
+
else if (rebufferRatio < 2) smoothnessScore = 70;
|
|
200
|
+
else if (rebufferRatio < 5) smoothnessScore = 50;
|
|
201
|
+
else smoothnessScore = 30;
|
|
202
|
+
}
|
|
203
|
+
let successScore = 100;
|
|
204
|
+
if (exitType === "error") {
|
|
205
|
+
successScore = 0;
|
|
206
|
+
} else if (errorCount > 0) {
|
|
207
|
+
successScore = Math.max(0, 100 - errorCount * 10);
|
|
208
|
+
}
|
|
209
|
+
let qualityScore = 80;
|
|
210
|
+
if (maxBitrate > 4e6) qualityScore = 100;
|
|
211
|
+
else if (maxBitrate > 2e6) qualityScore = 90;
|
|
212
|
+
else if (maxBitrate > 1e6) qualityScore = 75;
|
|
213
|
+
else if (maxBitrate > 5e5) qualityScore = 60;
|
|
214
|
+
else if (maxBitrate > 0) qualityScore = 40;
|
|
215
|
+
const qoeScore = successScore * 0.3 + startupScore * 0.25 + smoothnessScore * 0.3 + qualityScore * 0.15;
|
|
216
|
+
return Math.round(qoeScore);
|
|
217
|
+
}
|
|
218
|
+
function isDevelopment() {
|
|
219
|
+
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname.includes(".local");
|
|
220
|
+
}
|
|
221
|
+
function safeStringify(data) {
|
|
222
|
+
try {
|
|
223
|
+
return JSON.stringify(data);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return "{}";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/index.ts
|
|
230
|
+
var PLUGIN_VERSION = "0.1.0";
|
|
231
|
+
var PLUGIN_NAME = "scarlett-player";
|
|
232
|
+
var DEFAULT_CONFIG = {
|
|
233
|
+
heartbeatInterval: 1e4,
|
|
234
|
+
errorSampleRate: 1,
|
|
235
|
+
disableInDev: false
|
|
236
|
+
};
|
|
237
|
+
function createAnalyticsPlugin(config) {
|
|
238
|
+
if (!config.beaconUrl) {
|
|
239
|
+
throw new Error("Analytics plugin requires beaconUrl");
|
|
240
|
+
}
|
|
241
|
+
if (!config.videoId) {
|
|
242
|
+
throw new Error("Analytics plugin requires videoId");
|
|
243
|
+
}
|
|
244
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
245
|
+
let api = null;
|
|
246
|
+
let session;
|
|
247
|
+
let heartbeatTimer = null;
|
|
248
|
+
let lastHeartbeatTime = 0;
|
|
249
|
+
let isRebuffering = false;
|
|
250
|
+
let rebufferStartTime = null;
|
|
251
|
+
let pauseStartTime = null;
|
|
252
|
+
let cleanupFns = [];
|
|
253
|
+
function initSession() {
|
|
254
|
+
return {
|
|
255
|
+
viewId: generateId(),
|
|
256
|
+
sessionId: getSessionId(),
|
|
257
|
+
viewerId: mergedConfig.viewerId || getAnonymousViewerId(),
|
|
258
|
+
viewStart: Date.now(),
|
|
259
|
+
playRequestTime: null,
|
|
260
|
+
firstFrameTime: null,
|
|
261
|
+
viewEnd: null,
|
|
262
|
+
watchTime: 0,
|
|
263
|
+
playTime: 0,
|
|
264
|
+
pauseCount: 0,
|
|
265
|
+
pauseDuration: 0,
|
|
266
|
+
seekCount: 0,
|
|
267
|
+
startupTime: null,
|
|
268
|
+
rebufferCount: 0,
|
|
269
|
+
rebufferDuration: 0,
|
|
270
|
+
errorCount: 0,
|
|
271
|
+
errors: [],
|
|
272
|
+
bitrateHistory: [],
|
|
273
|
+
qualityChanges: 0,
|
|
274
|
+
maxBitrate: 0,
|
|
275
|
+
avgBitrate: 0,
|
|
276
|
+
playbackState: "loading",
|
|
277
|
+
exitType: null
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function sendBeacon(eventType, data = {}) {
|
|
281
|
+
if (mergedConfig.disableInDev && isDevelopment()) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (eventType === "error" && Math.random() > (mergedConfig.errorSampleRate ?? 1)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const payload = {
|
|
288
|
+
// Event info
|
|
289
|
+
event: eventType,
|
|
290
|
+
timestamp: Date.now(),
|
|
291
|
+
// View context
|
|
292
|
+
viewId: session.viewId,
|
|
293
|
+
sessionId: session.sessionId,
|
|
294
|
+
viewerId: session.viewerId,
|
|
295
|
+
// Video context
|
|
296
|
+
videoId: mergedConfig.videoId,
|
|
297
|
+
videoTitle: mergedConfig.videoTitle,
|
|
298
|
+
isLive: mergedConfig.isLive ?? api?.getState("live") ?? false,
|
|
299
|
+
// Player context
|
|
300
|
+
playerVersion: PLUGIN_VERSION,
|
|
301
|
+
playerName: PLUGIN_NAME,
|
|
302
|
+
// Environment
|
|
303
|
+
browser: getBrowserInfo().name,
|
|
304
|
+
os: getOSInfo().name,
|
|
305
|
+
deviceType: getDeviceType(),
|
|
306
|
+
screenSize: getScreenSize(),
|
|
307
|
+
playerSize: getPlayerSize(api?.container ?? null),
|
|
308
|
+
connectionType: getConnectionType(),
|
|
309
|
+
// Custom dimensions
|
|
310
|
+
...mergedConfig.customDimensions,
|
|
311
|
+
// Event-specific data
|
|
312
|
+
...data
|
|
313
|
+
};
|
|
314
|
+
if (mergedConfig.customBeacon) {
|
|
315
|
+
mergedConfig.customBeacon(mergedConfig.beaconUrl, payload);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (navigator.sendBeacon) {
|
|
319
|
+
const blob = new Blob([safeStringify(payload)], {
|
|
320
|
+
type: "application/json"
|
|
321
|
+
});
|
|
322
|
+
navigator.sendBeacon(mergedConfig.beaconUrl, blob);
|
|
323
|
+
} else {
|
|
324
|
+
fetch(mergedConfig.beaconUrl, {
|
|
325
|
+
method: "POST",
|
|
326
|
+
headers: {
|
|
327
|
+
"Content-Type": "application/json",
|
|
328
|
+
...mergedConfig.apiKey ? { "X-API-Key": mergedConfig.apiKey } : {}
|
|
329
|
+
},
|
|
330
|
+
body: safeStringify(payload),
|
|
331
|
+
keepalive: true
|
|
332
|
+
}).catch(() => {
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function sendHeartbeat() {
|
|
337
|
+
if (!api) return;
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
const timeSinceLastHeartbeat = now - lastHeartbeatTime;
|
|
340
|
+
session.watchTime += timeSinceLastHeartbeat;
|
|
341
|
+
if (session.playbackState === "playing" && !isRebuffering) {
|
|
342
|
+
session.playTime += timeSinceLastHeartbeat;
|
|
343
|
+
}
|
|
344
|
+
if (session.bitrateHistory.length > 0) {
|
|
345
|
+
const totalBitrateTime = session.bitrateHistory.reduce((sum, b, i, arr) => {
|
|
346
|
+
const nextTime = i < arr.length - 1 ? arr[i + 1]?.time : now;
|
|
347
|
+
const duration = nextTime - b.time;
|
|
348
|
+
return sum + b.bitrate * duration;
|
|
349
|
+
}, 0);
|
|
350
|
+
session.avgBitrate = Math.round(totalBitrateTime / session.watchTime);
|
|
351
|
+
}
|
|
352
|
+
const state = {
|
|
353
|
+
currentTime: api.getState("currentTime"),
|
|
354
|
+
duration: api.getState("duration")
|
|
355
|
+
};
|
|
356
|
+
sendBeacon("heartbeat", {
|
|
357
|
+
watchTime: session.watchTime,
|
|
358
|
+
playTime: session.playTime,
|
|
359
|
+
currentTime: state.currentTime,
|
|
360
|
+
duration: state.duration,
|
|
361
|
+
rebufferCount: session.rebufferCount,
|
|
362
|
+
rebufferDuration: session.rebufferDuration,
|
|
363
|
+
avgBitrate: session.avgBitrate,
|
|
364
|
+
qoeScore: getQoEScore()
|
|
365
|
+
});
|
|
366
|
+
lastHeartbeatTime = now;
|
|
367
|
+
}
|
|
368
|
+
function getQoEScore() {
|
|
369
|
+
return calculateQoEScore({
|
|
370
|
+
startupTime: session.startupTime,
|
|
371
|
+
rebufferDuration: session.rebufferDuration,
|
|
372
|
+
watchTime: session.watchTime,
|
|
373
|
+
maxBitrate: session.maxBitrate,
|
|
374
|
+
exitType: session.exitType,
|
|
375
|
+
errorCount: session.errorCount
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function sendViewEnd() {
|
|
379
|
+
if (!api) return;
|
|
380
|
+
session.viewEnd = Date.now();
|
|
381
|
+
const state = {
|
|
382
|
+
currentTime: api.getState("currentTime"),
|
|
383
|
+
duration: api.getState("duration")
|
|
384
|
+
};
|
|
385
|
+
const completionRate = state.duration ? state.currentTime / state.duration * 100 : 0;
|
|
386
|
+
sendBeacon("viewEnd", {
|
|
387
|
+
watchTime: session.watchTime,
|
|
388
|
+
playTime: session.playTime,
|
|
389
|
+
startupTime: session.startupTime,
|
|
390
|
+
rebufferCount: session.rebufferCount,
|
|
391
|
+
rebufferDuration: session.rebufferDuration,
|
|
392
|
+
rebufferRatio: session.watchTime > 0 ? session.rebufferDuration / session.watchTime * 100 : 0,
|
|
393
|
+
avgBitrate: session.avgBitrate,
|
|
394
|
+
maxBitrate: session.maxBitrate,
|
|
395
|
+
qualityChanges: session.qualityChanges,
|
|
396
|
+
pauseCount: session.pauseCount,
|
|
397
|
+
pauseDuration: session.pauseDuration,
|
|
398
|
+
seekCount: session.seekCount,
|
|
399
|
+
errorCount: session.errorCount,
|
|
400
|
+
exitType: session.exitType,
|
|
401
|
+
qoeScore: getQoEScore(),
|
|
402
|
+
completionRate
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
function onPlayRequest() {
|
|
406
|
+
session.playRequestTime = Date.now();
|
|
407
|
+
sendBeacon("playRequest");
|
|
408
|
+
}
|
|
409
|
+
function onPlaying() {
|
|
410
|
+
const now = Date.now();
|
|
411
|
+
if (session.firstFrameTime === null) {
|
|
412
|
+
session.firstFrameTime = now;
|
|
413
|
+
session.startupTime = session.playRequestTime ? now - session.playRequestTime : null;
|
|
414
|
+
sendBeacon("videoStart", {
|
|
415
|
+
startupTime: session.startupTime
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (isRebuffering && rebufferStartTime) {
|
|
419
|
+
const rebufferDuration = now - rebufferStartTime;
|
|
420
|
+
session.rebufferDuration += rebufferDuration;
|
|
421
|
+
isRebuffering = false;
|
|
422
|
+
rebufferStartTime = null;
|
|
423
|
+
sendBeacon("rebufferEnd", {
|
|
424
|
+
duration: rebufferDuration,
|
|
425
|
+
totalRebufferTime: session.rebufferDuration
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (pauseStartTime) {
|
|
429
|
+
const pauseDuration = now - pauseStartTime;
|
|
430
|
+
session.pauseDuration += pauseDuration;
|
|
431
|
+
pauseStartTime = null;
|
|
432
|
+
}
|
|
433
|
+
session.playbackState = "playing";
|
|
434
|
+
}
|
|
435
|
+
function onPause() {
|
|
436
|
+
if (!api) return;
|
|
437
|
+
session.pauseCount++;
|
|
438
|
+
session.playbackState = "paused";
|
|
439
|
+
pauseStartTime = Date.now();
|
|
440
|
+
sendBeacon("pause", {
|
|
441
|
+
currentTime: api.getState("currentTime")
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function onWaiting() {
|
|
445
|
+
if (!api) return;
|
|
446
|
+
if (session.firstFrameTime !== null && !isRebuffering) {
|
|
447
|
+
isRebuffering = true;
|
|
448
|
+
rebufferStartTime = Date.now();
|
|
449
|
+
session.rebufferCount++;
|
|
450
|
+
sendBeacon("rebufferStart", {
|
|
451
|
+
rebufferCount: session.rebufferCount,
|
|
452
|
+
currentTime: api.getState("currentTime")
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function onSeeking() {
|
|
457
|
+
if (!api) return;
|
|
458
|
+
session.seekCount++;
|
|
459
|
+
sendBeacon("seeking", {
|
|
460
|
+
seekCount: session.seekCount,
|
|
461
|
+
seekTo: api.getState("currentTime")
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function onEnded() {
|
|
465
|
+
session.playbackState = "ended";
|
|
466
|
+
session.exitType = "completed";
|
|
467
|
+
sendViewEnd();
|
|
468
|
+
}
|
|
469
|
+
function onError(payload) {
|
|
470
|
+
const error = payload.error;
|
|
471
|
+
session.errorCount++;
|
|
472
|
+
const errorEvent = {
|
|
473
|
+
time: Date.now(),
|
|
474
|
+
type: error.name || "Error",
|
|
475
|
+
message: error.message || "Unknown error",
|
|
476
|
+
fatal: error.fatal ?? false
|
|
477
|
+
};
|
|
478
|
+
session.errors.push(errorEvent);
|
|
479
|
+
sendBeacon("error", {
|
|
480
|
+
errorType: errorEvent.type,
|
|
481
|
+
errorMessage: errorEvent.message,
|
|
482
|
+
errorCode: error.code,
|
|
483
|
+
fatal: errorEvent.fatal
|
|
484
|
+
});
|
|
485
|
+
if (errorEvent.fatal) {
|
|
486
|
+
session.playbackState = "error";
|
|
487
|
+
session.exitType = "error";
|
|
488
|
+
sendViewEnd();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function onQualityChange(payload) {
|
|
492
|
+
if (!api) return;
|
|
493
|
+
const now = Date.now();
|
|
494
|
+
session.qualityChanges++;
|
|
495
|
+
const qualities = api.getState("qualities");
|
|
496
|
+
const currentQuality = qualities.find((q) => q.id === payload.quality);
|
|
497
|
+
if (currentQuality) {
|
|
498
|
+
const bitrateChange = {
|
|
499
|
+
time: now,
|
|
500
|
+
bitrate: currentQuality.bitrate,
|
|
501
|
+
width: currentQuality.width,
|
|
502
|
+
height: currentQuality.height
|
|
503
|
+
};
|
|
504
|
+
session.bitrateHistory.push(bitrateChange);
|
|
505
|
+
if (currentQuality.bitrate > session.maxBitrate) {
|
|
506
|
+
session.maxBitrate = currentQuality.bitrate;
|
|
507
|
+
}
|
|
508
|
+
sendBeacon("qualityChange", {
|
|
509
|
+
bitrate: currentQuality.bitrate,
|
|
510
|
+
width: currentQuality.width,
|
|
511
|
+
height: currentQuality.height,
|
|
512
|
+
auto: payload.auto
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function onVisibilityChange() {
|
|
517
|
+
if (document.hidden) {
|
|
518
|
+
session.exitType = "background";
|
|
519
|
+
sendHeartbeat();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function onBeforeUnload() {
|
|
523
|
+
if (!session.exitType) {
|
|
524
|
+
session.exitType = "abandoned";
|
|
525
|
+
}
|
|
526
|
+
sendViewEnd();
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
id: "analytics",
|
|
530
|
+
name: "Analytics",
|
|
531
|
+
version: PLUGIN_VERSION,
|
|
532
|
+
type: "analytics",
|
|
533
|
+
description: "Quality of Experience and engagement analytics",
|
|
534
|
+
async init(pluginApi) {
|
|
535
|
+
api = pluginApi;
|
|
536
|
+
session = initSession();
|
|
537
|
+
lastHeartbeatTime = Date.now();
|
|
538
|
+
sendBeacon("viewStart");
|
|
539
|
+
const unsubPlay = api.on("playback:play", () => {
|
|
540
|
+
onPlayRequest();
|
|
541
|
+
onPlaying();
|
|
542
|
+
});
|
|
543
|
+
const unsubPause = api.on("playback:pause", onPause);
|
|
544
|
+
const unsubWaiting = api.on("media:waiting", onWaiting);
|
|
545
|
+
const unsubSeeking = api.on("playback:seeking", onSeeking);
|
|
546
|
+
const unsubEnded = api.on("playback:ended", onEnded);
|
|
547
|
+
const unsubError = api.on("media:error", onError);
|
|
548
|
+
const unsubQuality = api.on("quality:change", onQualityChange);
|
|
549
|
+
cleanupFns.push(
|
|
550
|
+
unsubPlay,
|
|
551
|
+
unsubPause,
|
|
552
|
+
unsubWaiting,
|
|
553
|
+
unsubSeeking,
|
|
554
|
+
unsubEnded,
|
|
555
|
+
unsubError,
|
|
556
|
+
unsubQuality
|
|
557
|
+
);
|
|
558
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
559
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
560
|
+
cleanupFns.push(() => {
|
|
561
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
562
|
+
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
563
|
+
});
|
|
564
|
+
heartbeatTimer = setInterval(
|
|
565
|
+
sendHeartbeat,
|
|
566
|
+
mergedConfig.heartbeatInterval || 1e4
|
|
567
|
+
);
|
|
568
|
+
api.logger.info("Analytics plugin initialized", {
|
|
569
|
+
viewId: session.viewId,
|
|
570
|
+
videoId: mergedConfig.videoId
|
|
571
|
+
});
|
|
572
|
+
},
|
|
573
|
+
async destroy() {
|
|
574
|
+
if (heartbeatTimer) {
|
|
575
|
+
clearInterval(heartbeatTimer);
|
|
576
|
+
heartbeatTimer = null;
|
|
577
|
+
}
|
|
578
|
+
if (!session.viewEnd) {
|
|
579
|
+
session.exitType = session.exitType || "abandoned";
|
|
580
|
+
sendViewEnd();
|
|
581
|
+
}
|
|
582
|
+
cleanupFns.forEach((fn) => fn());
|
|
583
|
+
cleanupFns = [];
|
|
584
|
+
api?.logger.info("Analytics plugin destroyed");
|
|
585
|
+
api = null;
|
|
586
|
+
},
|
|
587
|
+
// === Public API ===
|
|
588
|
+
getViewId() {
|
|
589
|
+
return session.viewId;
|
|
590
|
+
},
|
|
591
|
+
getSessionId() {
|
|
592
|
+
return session.sessionId;
|
|
593
|
+
},
|
|
594
|
+
getQoEScore() {
|
|
595
|
+
return getQoEScore();
|
|
596
|
+
},
|
|
597
|
+
getMetrics() {
|
|
598
|
+
return { ...session };
|
|
599
|
+
},
|
|
600
|
+
trackEvent(name, data = {}) {
|
|
601
|
+
sendBeacon(`custom:${name}`, data);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
var index_default = createAnalyticsPlugin;
|
|
606
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
607
|
+
0 && (module.exports = {
|
|
608
|
+
createAnalyticsPlugin
|
|
609
|
+
});
|