@eyevinn/player-analytics-client-sdk-web 0.3.0 → 0.4.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 +218 -47
- package/dist/index.d.ts +2 -0
- package/dist/index.js +508 -85
- package/dist/index.js.map +1 -1
- package/dist/module.js +508 -85
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +26 -7
- package/dist/types.d.ts.map +1 -1
- package/index.ts +7 -0
- package/package.json +5 -1
- package/spec/PlayerAnalytics.spec.ts +338 -0
- package/spec/PlayerAnalyticsConnector.spec.ts +1474 -0
- package/spec/Reporter.spec.ts +825 -0
- package/spec/support/jasmine.json +7 -0
- package/tsconfig.json +1 -1
package/Readme.md
CHANGED
|
@@ -26,32 +26,55 @@ The quickest way to get started is to setup an EPAS compatible backend in [Eyevi
|
|
|
26
26
|
import { PlayerAnalyticsConnector } from "@eyevinn/player-analytics-client-sdk-web";
|
|
27
27
|
|
|
28
28
|
// Create your instance and set the analytics eventsink endpoint
|
|
29
|
-
const
|
|
29
|
+
const analytics = new PlayerAnalyticsConnector(
|
|
30
30
|
"https://your-eventsink-url.io"
|
|
31
31
|
);
|
|
32
32
|
|
|
33
33
|
// Initiate the analytics with the base data needed
|
|
34
|
-
// This will create
|
|
34
|
+
// This will create your session in the backend
|
|
35
35
|
try {
|
|
36
|
-
await
|
|
36
|
+
await analytics.init({
|
|
37
37
|
sessionId: "generated-unique-uuid-session-id",
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// Get your video element from the site, or your video player of choice
|
|
41
41
|
const videoElement = document.querySelector("video");
|
|
42
42
|
// Load the analytics library with the video element to fetch the events
|
|
43
|
-
|
|
43
|
+
analytics.load(videoElement);
|
|
44
44
|
} catch (err) {
|
|
45
45
|
console.error(err);
|
|
46
46
|
// Remove event listeners and heartbeat timers if init fails.
|
|
47
|
-
|
|
47
|
+
analytics.deinit();
|
|
48
48
|
}
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
#### Non-blocking `init()` and autoplay
|
|
52
|
+
|
|
53
|
+
`load()` does **not** have to wait for `init()` to resolve. You may call `load()`
|
|
54
|
+
immediately after `init()` starts (without awaiting it) — any events produced
|
|
55
|
+
before the handshake completes are queued and flushed, in order, once the session
|
|
56
|
+
is established:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
const analytics = new PlayerAnalyticsConnector("https://your-eventsink-url.io");
|
|
60
|
+
|
|
61
|
+
// Start the handshake but don't await it...
|
|
62
|
+
analytics.init({ sessionId: "generated-unique-uuid-session-id" });
|
|
63
|
+
// ...attach the player right away so no early events are missed.
|
|
64
|
+
analytics.load(videoElement);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This matters for **autoplay**: if the video starts playing while `init()` is still
|
|
68
|
+
in flight, the `playing` event fires before the SDK's listeners are attached. To
|
|
69
|
+
avoid losing it, `load()` inspects the element and, if it is already playing,
|
|
70
|
+
emits a `playing` event itself and starts the heartbeat (queued/deferred until the
|
|
71
|
+
handshake completes). Attaching with `load()` as early as possible ensures the
|
|
72
|
+
`playing` event and heartbeats are captured even for autoplaying content.
|
|
73
|
+
|
|
74
|
+
Due to bitrate changes not being reported, and errors not being reported in any descriptive way, there is a possibility to do separate calls for these events - which you may trigger based on your video player of choice following these examples.
|
|
52
75
|
|
|
53
76
|
```js
|
|
54
|
-
|
|
77
|
+
analytics.reportBitrateChange({
|
|
55
78
|
bitrate: 246.440, // bitrate in Kbps
|
|
56
79
|
width: 320, // optional, video width in pixels
|
|
57
80
|
height: 136, // optional, video height in pixels
|
|
@@ -61,8 +84,8 @@ playerAnalytics.reportBitrateChange({
|
|
|
61
84
|
```
|
|
62
85
|
|
|
63
86
|
```js
|
|
64
|
-
// error is fatal, i.e. sends
|
|
65
|
-
|
|
87
|
+
// error is fatal, i.e. sends a stopped event as well
|
|
88
|
+
analytics.reportError({
|
|
66
89
|
category: "", // optional, eg. NETWORK, DECODER, etc.
|
|
67
90
|
code: "",
|
|
68
91
|
message: "", // optional
|
|
@@ -70,7 +93,7 @@ playerAnalytics.reportError({
|
|
|
70
93
|
});
|
|
71
94
|
|
|
72
95
|
// warning is not fatal
|
|
73
|
-
|
|
96
|
+
analytics.reportWarning({
|
|
74
97
|
category: "", // optional, eg. NETWORK, DECODER, etc.
|
|
75
98
|
code: "",
|
|
76
99
|
message: "", // optional
|
|
@@ -79,13 +102,15 @@ playerAnalytics.reportWarning({
|
|
|
79
102
|
```
|
|
80
103
|
|
|
81
104
|
```js
|
|
82
|
-
// when leaving the player, to stop the analytics in a correct
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
// when leaving the player, to stop the analytics in a correct manner
|
|
106
|
+
analytics.reportStop();
|
|
107
|
+
analytics.destroy();
|
|
85
108
|
```
|
|
86
109
|
|
|
87
110
|
### Manual Event Triggering
|
|
88
111
|
|
|
112
|
+
Use the `PlayerAnalytics` class directly for full control over when events are sent.
|
|
113
|
+
|
|
89
114
|
```js
|
|
90
115
|
import { PlayerAnalytics } from "@eyevinn/player-analytics-client-sdk-web";
|
|
91
116
|
|
|
@@ -93,9 +118,6 @@ import { PlayerAnalytics } from "@eyevinn/player-analytics-client-sdk-web";
|
|
|
93
118
|
const playerAnalytics = new PlayerAnalytics("https://your-eventsink-url.io");
|
|
94
119
|
await playerAnalytics.initiateAnalyticsReporter({
|
|
95
120
|
sessionId: "generated-unique-uuid-session-id",
|
|
96
|
-
contentId: "big-buck-bunny-720",
|
|
97
|
-
contentUrl:
|
|
98
|
-
"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4",
|
|
99
121
|
});
|
|
100
122
|
|
|
101
123
|
// then trigger the method calls accordingly, e.g.
|
|
@@ -104,8 +126,8 @@ videoElement.addEventListener("play", () => {
|
|
|
104
126
|
playerAnalytics.playing({
|
|
105
127
|
event: "playing",
|
|
106
128
|
timestamp: Date.now(),
|
|
107
|
-
playhead:
|
|
108
|
-
duration:
|
|
129
|
+
playhead: videoElement.currentTime,
|
|
130
|
+
duration: videoElement.duration,
|
|
109
131
|
sessionId: "generated-unique-uuid-session-id",
|
|
110
132
|
});
|
|
111
133
|
});
|
|
@@ -113,65 +135,214 @@ videoElement.addEventListener("play", () => {
|
|
|
113
135
|
|
|
114
136
|
### Report Metadata
|
|
115
137
|
|
|
116
|
-
You can report
|
|
117
|
-
In this example we're using hls.js
|
|
138
|
+
You can report metadata at a later stage in your code and tailor the callbacks for your tech stack, for example when you have parsed your manifest.
|
|
139
|
+
In this example we're using hls.js with the `PlayerAnalyticsConnector`:
|
|
118
140
|
|
|
119
141
|
```js
|
|
120
142
|
import Hls from "hls.js";
|
|
121
143
|
import { PlayerAnalyticsConnector } from "@eyevinn/player-analytics-client-sdk-web";
|
|
122
144
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
await playerAnalytics.initiateAnalyticsReporter({
|
|
145
|
+
const analytics = new PlayerAnalyticsConnector("https://your-eventsink-url.io");
|
|
146
|
+
await analytics.init({
|
|
126
147
|
sessionId: "generated-unique-uuid-session-id",
|
|
127
148
|
});
|
|
128
149
|
|
|
129
150
|
const hls = new Hls();
|
|
130
151
|
const videoElement = document.querySelector("video");
|
|
131
152
|
|
|
153
|
+
analytics.load(videoElement);
|
|
132
154
|
hls.attachMedia(videoElement);
|
|
133
|
-
hls.
|
|
155
|
+
hls.loadSource(src);
|
|
134
156
|
hls.on(Hls.Events.LEVEL_LOADED, (event, data) => {
|
|
135
|
-
|
|
136
|
-
live: data?.details?.
|
|
157
|
+
analytics.reportMetadata({
|
|
158
|
+
live: data?.details?.live,
|
|
159
|
+
contentTitle: "My Video",
|
|
160
|
+
contentUrl: src,
|
|
137
161
|
});
|
|
138
162
|
});
|
|
139
163
|
```
|
|
140
164
|
|
|
141
|
-
These are the available keys for metadata, which can be sent anytime between
|
|
165
|
+
These are the available keys for metadata, which can be sent anytime between an `init` and a `stopped` call.
|
|
142
166
|
|
|
143
167
|
```ts
|
|
144
|
-
export
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
export type TMetadataEventPayload = {
|
|
169
|
+
live?: boolean;
|
|
170
|
+
contentTitle?: string;
|
|
171
|
+
contentId?: string;
|
|
172
|
+
contentUrl?: string;
|
|
173
|
+
drmType?: string;
|
|
174
|
+
userId?: string;
|
|
175
|
+
deviceId?: string;
|
|
176
|
+
deviceModel?: string;
|
|
177
|
+
deviceType?: string;
|
|
178
|
+
[key: string]: string | boolean; // additional custom metadata
|
|
154
179
|
}
|
|
155
180
|
```
|
|
156
181
|
|
|
157
|
-
###
|
|
182
|
+
### Events Tracked
|
|
183
|
+
|
|
184
|
+
When you call `analytics.load(videoElement)` on `PlayerAnalyticsConnector`, the SDK attaches listeners via [`@eyevinn/media-event-filter`](https://github.com/Eyevinn/media-event-filter) and emits the following EPAS events automatically:
|
|
185
|
+
|
|
186
|
+
| Native trigger | EPAS event |
|
|
187
|
+
|----------------|------------|
|
|
188
|
+
| Media element load | `loading` (sent on `load()`) |
|
|
189
|
+
| First playable state | `loaded` |
|
|
190
|
+
| `playing` (or already playing at `load()`) | `playing` (also starts the heartbeat) |
|
|
191
|
+
| `pause` | `paused` |
|
|
192
|
+
| `seeking` | `seeking` |
|
|
193
|
+
| `seeked` | `seeked` |
|
|
194
|
+
| Buffer stall | `buffering` |
|
|
195
|
+
| Buffer recovery | `buffered` |
|
|
196
|
+
| `ended` | `stopped` (`reason: "ended"`) |
|
|
197
|
+
| Page close / reload (`pagehide`, `visibilitychange` → hidden) | `stopped` (`reason: "aborted"`) |
|
|
158
198
|
|
|
159
|
-
|
|
199
|
+
`bitrate_changed`, `error`, `warning`, `metadata`, and manual `stopped` (`reason: "aborted"`) are **not** auto-detected — call the corresponding `report*` methods when you have the data. A `heartbeat` event is sent every `heartbeatInterval` ms while the player is in the `playing` state (see [Init Parameters](#init-parameters) below).
|
|
160
200
|
|
|
161
|
-
-
|
|
162
|
-
- `debug`, default false, triggers output to dev console rather than actual http posts.
|
|
201
|
+
### Unload-time delivery (page close / reload)
|
|
163
202
|
|
|
164
|
-
|
|
203
|
+
When the page is closed or reloaded mid-session, the SDK delivers the final `stopped` event (`reason: "aborted"`) over [`navigator.sendBeacon`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon) instead of a normal fetch. The beacon is sent as a `text/plain` JSON body with **no `X-EPAS-*` headers**, which keeps it a CORS "simple request" so the browser does not issue a preflight — a preflight is dropped while the page unloads, which is why the `stopped` event was previously lost. The eventsink reads the event from the JSON body (`body.event`), so this is a fully supported way to send unload-time events.
|
|
204
|
+
|
|
205
|
+
Where `navigator.sendBeacon` is unavailable, the SDK falls back to `fetch(..., { keepalive: true })` with the same `text/plain` JSON body.
|
|
206
|
+
|
|
207
|
+
The unload listeners are attached automatically on `load()` and removed on `deinit()` / `destroy()`. The `stopped` event is only sent once per session, so an explicit `reportStop()`, a `reportError()`, or an `ended` event already reported for the session suppresses the unload beacon.
|
|
208
|
+
|
|
209
|
+
### Constructor Parameters
|
|
210
|
+
|
|
211
|
+
Both `PlayerAnalyticsConnector` and `PlayerAnalytics` share the same constructor signature `(eventsinkUrl, debug?, onError?)`:
|
|
212
|
+
|
|
213
|
+
| Parameter | Type | Default | Description |
|
|
214
|
+
|-----------|------|---------|-------------|
|
|
215
|
+
| `eventsinkUrl` | `string` | (required) | URL to your eventsink endpoint |
|
|
216
|
+
| `debug` | `boolean` | `false` | When true, logs events to console instead of sending HTTP requests |
|
|
217
|
+
| `onError` | `TOnSendError` | `undefined` | Optional callback invoked when a send fails. Falls back to `console.warn` if not provided |
|
|
218
|
+
|
|
219
|
+
### Init Parameters
|
|
165
220
|
|
|
166
221
|
```ts
|
|
167
|
-
export interface
|
|
168
|
-
sessionId?: string;
|
|
169
|
-
shardId?: string;
|
|
170
|
-
|
|
171
|
-
hearbeatInterval?: number; //Defaults to 30_000 (ms)
|
|
222
|
+
export interface IPlayerAnalyticsConnectorInitOptions {
|
|
223
|
+
sessionId?: string; // Generated by the backend if not provided
|
|
224
|
+
shardId?: string; // Optional grouping key for sharding events in the pipeline
|
|
225
|
+
heartbeatInterval?: number; // Defaults to HEARTBEAT_INTERVAL (see src/utils/constants.ts)
|
|
172
226
|
}
|
|
173
227
|
```
|
|
174
228
|
|
|
229
|
+
Example:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
const analytics = new PlayerAnalyticsConnector(
|
|
233
|
+
"https://eyevinnlab-epasdev.eyevinn-player-analytics-eventsink.auto.prod.osaas.io/"
|
|
234
|
+
);
|
|
235
|
+
await analytics.init({
|
|
236
|
+
sessionId: `demo-page-${Date.now()}`,
|
|
237
|
+
shardId: "myshard1",
|
|
238
|
+
heartbeatInterval: 10000,
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Error Handling
|
|
243
|
+
|
|
244
|
+
By default, send failures are logged to `console.warn`. You can provide an optional `onError` callback (see [Constructor Parameters](#constructor-parameters) above) to handle errors programmatically:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import {
|
|
248
|
+
PlayerAnalyticsConnector,
|
|
249
|
+
TAnalyticsSendError,
|
|
250
|
+
TPlayerAnalyticsEvent,
|
|
251
|
+
} from "@eyevinn/player-analytics-client-sdk-web";
|
|
252
|
+
|
|
253
|
+
const analytics = new PlayerAnalyticsConnector(
|
|
254
|
+
"https://your-eventsink-url.io",
|
|
255
|
+
false, // debug
|
|
256
|
+
(error: TAnalyticsSendError, event: TPlayerAnalyticsEvent) => {
|
|
257
|
+
// error.status - HTTP status code (e.g. 404), undefined for network errors
|
|
258
|
+
// error.statusText - HTTP status text (e.g. "Not Found")
|
|
259
|
+
// error.message - Human-readable error description
|
|
260
|
+
// event - the analytics event that failed to send
|
|
261
|
+
console.error(`Analytics send failed: ${error.message}`, event.event);
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### API Reference
|
|
267
|
+
|
|
268
|
+
#### PlayerAnalyticsConnector
|
|
269
|
+
|
|
270
|
+
High-level API that automatically listens to `HTMLVideoElement` events.
|
|
271
|
+
|
|
272
|
+
| Method | Description |
|
|
273
|
+
|--------|-------------|
|
|
274
|
+
| `init(options)` | Connect to eventsink and create a session |
|
|
275
|
+
| `load(videoElement)` | Attach to a video element and start listening for events |
|
|
276
|
+
| `reportBitrateChange(payload)` | Report a bitrate change event |
|
|
277
|
+
| `reportError(payload)` | Report a fatal error (also sends a stopped event) |
|
|
278
|
+
| `reportWarning(payload)` | Report a non-fatal warning |
|
|
279
|
+
| `reportMetadata(payload)` | Report metadata (content info, device info, etc.) |
|
|
280
|
+
| `reportStop()` | Report that playback was stopped by the user |
|
|
281
|
+
| `deinit()` | Stop heartbeat and remove event listeners (keeps instance reusable) |
|
|
282
|
+
| `destroy()` | Fully tear down the instance |
|
|
283
|
+
|
|
284
|
+
#### PlayerAnalytics
|
|
285
|
+
|
|
286
|
+
Low-level API implementing the [EPAS PlayerAnalyticsClientModule](https://github.com/Eyevinn/player-analytics-specification) interface. `initiateAnalyticsReporter()` must be called before any event method — other calls no-op with a warning until then.
|
|
287
|
+
|
|
288
|
+
**Lifecycle**
|
|
289
|
+
|
|
290
|
+
| Method | Description |
|
|
291
|
+
|--------|-------------|
|
|
292
|
+
| `initiateAnalyticsReporter(options)` | Connect to eventsink and create a session (required first call). Accepts `IPlayerAnalyticsInitOptions`. |
|
|
293
|
+
| `destroy()` | Tear down the reporter and discard queued events. |
|
|
294
|
+
|
|
295
|
+
**EPAS event methods**
|
|
296
|
+
|
|
297
|
+
Each accepts a full EPAS event object.
|
|
298
|
+
|
|
299
|
+
| Method | Event Type |
|
|
300
|
+
|--------|------------|
|
|
301
|
+
| `init(data)` | `TInitEvent` |
|
|
302
|
+
| `heartbeat(data)` | `THeartbeatEvent` |
|
|
303
|
+
| `loading(data)` | `TLoadingEvent` |
|
|
304
|
+
| `loaded(data)` | `TLoadedEvent` |
|
|
305
|
+
| `playing(data)` | `TPlayingEvent` |
|
|
306
|
+
| `pause(data)` | `TPausedEvent` |
|
|
307
|
+
| `buffering(data)` | `TBufferingEvent` |
|
|
308
|
+
| `buffered(data)` | `TBufferedEvent` |
|
|
309
|
+
| `seeking(data)` | `TSeekingEvent` |
|
|
310
|
+
| `seeked(data)` | `TSeekedEvent` |
|
|
311
|
+
| `bitrateChanged(data)` | `TBitrateChangedEvent` |
|
|
312
|
+
| `error(data)` | `TErrorEvent` |
|
|
313
|
+
| `warning(data)` | `TWarningEvent` |
|
|
314
|
+
| `stopped(data)` | `TStoppedEvent` |
|
|
315
|
+
| `metadata(data)` | `TMetadataEvent` |
|
|
316
|
+
|
|
317
|
+
#### Exports
|
|
318
|
+
|
|
319
|
+
**Classes**
|
|
320
|
+
|
|
321
|
+
| Class | Description |
|
|
322
|
+
|-------|-------------|
|
|
323
|
+
| `PlayerAnalyticsConnector` | High-level, auto-listening API (see above) |
|
|
324
|
+
| `PlayerAnalytics` | Low-level, EPAS-compliant API (see above) |
|
|
325
|
+
|
|
326
|
+
**Types**
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import type {
|
|
330
|
+
IPlayerAnalyticsConnectorInitOptions,
|
|
331
|
+
IPlayerAnalyticsInitOptions,
|
|
332
|
+
TAnalyticsSendError,
|
|
333
|
+
TOnSendError,
|
|
334
|
+
TPlayerAnalyticsEvent,
|
|
335
|
+
} from "@eyevinn/player-analytics-client-sdk-web";
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
| Type | Description |
|
|
339
|
+
|------|-------------|
|
|
340
|
+
| `IPlayerAnalyticsConnectorInitOptions` | Options passed to `PlayerAnalyticsConnector.init()` — `{ sessionId?, shardId?, heartbeatInterval? }` |
|
|
341
|
+
| `IPlayerAnalyticsInitOptions` | Options passed to `PlayerAnalytics.initiateAnalyticsReporter()` — same shape as above |
|
|
342
|
+
| `TAnalyticsSendError` | `{ status?: number; statusText?: string; message: string }` — `status`/`statusText` are set on HTTP errors, absent on network errors |
|
|
343
|
+
| `TOnSendError` | `(error: TAnalyticsSendError, event: TPlayerAnalyticsEvent) => void` |
|
|
344
|
+
| `TPlayerAnalyticsEvent` | Union of all EPAS event types — re-exported from [`@eyevinn/player-analytics-specification`](https://github.com/Eyevinn/player-analytics-specification) |
|
|
345
|
+
|
|
175
346
|
## Development
|
|
176
347
|
|
|
177
348
|
Run the demo page by `npm start`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { PlayerAnalytics, IPlayerAnalyticsInitOptions, } from "./src/PlayerAnalytics";
|
|
2
2
|
export { PlayerAnalyticsConnector, IPlayerAnalyticsConnectorInitOptions, } from "./src/PlayerAnalyticsConnector";
|
|
3
|
+
export type { TAnalyticsSendError, TOnSendError, } from "./src/utils/Reporter";
|
|
4
|
+
export type { TPlayerAnalyticsEvent } from "@eyevinn/player-analytics-specification";
|