@eyevinn/player-analytics-client-sdk-web 0.2.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.
@@ -0,0 +1,29 @@
1
+ name: Deploy demo site
2
+ on:
3
+ push:
4
+ branches:
5
+ - master
6
+
7
+ jobs:
8
+ deploy:
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - uses: actions/checkout@v3
13
+ - uses: actions/setup-node@v3
14
+ with:
15
+ node-version: '18'
16
+ - name: install dependencies
17
+ run: |
18
+ npm install
19
+ npm run build:demo
20
+ - name: deploy to bucket
21
+ uses: jakejarvis/s3-sync-action@v0.5.1
22
+ with:
23
+ args: --follow-symlinks --delete
24
+ env:
25
+ AWS_S3_ENDPOINT: https://eyevinn-epasdemo.minio-minio.auto.prod.osaas.io
26
+ AWS_S3_BUCKET: 'www'
27
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
28
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
29
+ SOURCE_DIR: 'site/'
package/Readme.md CHANGED
@@ -1,11 +1,23 @@
1
+ [![Badge OSC](https://img.shields.io/badge/Evaluate-24243B?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTIiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl8yODIxXzMxNjcyKSIvPgo8Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSI3IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjIiLz4KPGRlZnM%2BCjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl8yODIxXzMxNjcyIiB4MT0iMTIiIHkxPSIwIiB4Mj0iMTIiIHkyPSIyNCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjQzE4M0ZGIi8%2BCjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzREQzlGRiIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM%2BCjwvc3ZnPgo%3D)](https://app.osaas.io/browse/eyevinn-player-analytics-eventsink)
2
+
1
3
  # Player Analytics Client SDK Web
2
4
 
3
- Part of [Eyevinn Player Analytics Specification](https://github.com/Eyevinn/player-analytics-specification). To be used together with [Eyevinn Player Analytics Eventsink](https://github.com/Eyevinn/player-analytics-eventsink)
5
+ Part of [Eyevinn Player Analytics Specification (EPAS)](https://github.com/Eyevinn/player-analytics-specification). To be used together with [Eyevinn Player Analytics Eventsink](https://github.com/Eyevinn/player-analytics-eventsink)
4
6
 
5
7
  ```
6
8
  npm install @eyevinn/player-analytics-client-sdk-web
7
9
  ```
8
10
 
11
+ ## EPAS backend in Open Source Cloud
12
+
13
+ The quickest way to get started is to setup an EPAS compatible backend in [Eyevinn Open Source Cloud](https://www.osaas.io)
14
+
15
+ ![EPAS pipeline in OSC](pipeline.png)
16
+
17
+ 1. Setup an Eventsink in OSC as described in the [documentation](https://docs.osaas.io/osaas.wiki/Service%3A-Player-Analytics-Eventsink.html).
18
+ 2. Setup an Analytics Worker and Clickhouse Database by following the instructions in the [documentation](https://docs.osaas.io/osaas.wiki/Service%3A-Player-Analytics-Worker.html)
19
+ 3. Add Clickhouse as datasource in Grafana and start visualizing the metrics
20
+
9
21
  ## Usage
10
22
 
11
23
  ### Automatic Event Listening
@@ -14,32 +26,55 @@ npm install @eyevinn/player-analytics-client-sdk-web
14
26
  import { PlayerAnalyticsConnector } from "@eyevinn/player-analytics-client-sdk-web";
15
27
 
16
28
  // Create your instance and set the analytics eventsink endpoint
17
- const playerAnalytics = new PlayerAnalyticsConnector(
29
+ const analytics = new PlayerAnalyticsConnector(
18
30
  "https://your-eventsink-url.io"
19
31
  );
20
32
 
21
33
  // Initiate the analytics with the base data needed
22
- // This will create you session in the backend
34
+ // This will create your session in the backend
23
35
  try {
24
- await playerAnalytics.init({
36
+ await analytics.init({
25
37
  sessionId: "generated-unique-uuid-session-id",
26
38
  });
27
39
 
28
40
  // Get your video element from the site, or your video player of choice
29
41
  const videoElement = document.querySelector("video");
30
42
  // Load the analytics library with the video element to fetch the events
31
- playerAnalytics.load(videoElement);
43
+ analytics.load(videoElement);
32
44
  } catch (err) {
33
45
  console.error(err);
34
46
  // Remove event listeners and heartbeat timers if init fails.
35
- playerAnalytics.deinit();
47
+ analytics.deinit();
36
48
  }
37
49
  ```
38
50
 
39
- 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 you video player of choice following these examples.
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.
40
75
 
41
76
  ```js
42
- playerAnalytics.reportBitrateChange({
77
+ analytics.reportBitrateChange({
43
78
  bitrate: 246.440, // bitrate in Kbps
44
79
  width: 320, // optional, video width in pixels
45
80
  height: 136, // optional, video height in pixels
@@ -49,8 +84,8 @@ playerAnalytics.reportBitrateChange({
49
84
  ```
50
85
 
51
86
  ```js
52
- // error is fatal, i.e. sends an end event as well
53
- playerAnalytics.reportError({
87
+ // error is fatal, i.e. sends a stopped event as well
88
+ analytics.reportError({
54
89
  category: "", // optional, eg. NETWORK, DECODER, etc.
55
90
  code: "",
56
91
  message: "", // optional
@@ -58,7 +93,7 @@ playerAnalytics.reportError({
58
93
  });
59
94
 
60
95
  // warning is not fatal
61
- playerAnalytics.reportWarning({
96
+ analytics.reportWarning({
62
97
  category: "", // optional, eg. NETWORK, DECODER, etc.
63
98
  code: "",
64
99
  message: "", // optional
@@ -67,13 +102,15 @@ playerAnalytics.reportWarning({
67
102
  ```
68
103
 
69
104
  ```js
70
- // when leaving the player, to stop the analytics in a correct manor
71
- playerAnalytics.reportStop();
72
- playerAnalytics.destroy();
105
+ // when leaving the player, to stop the analytics in a correct manner
106
+ analytics.reportStop();
107
+ analytics.destroy();
73
108
  ```
74
109
 
75
110
  ### Manual Event Triggering
76
111
 
112
+ Use the `PlayerAnalytics` class directly for full control over when events are sent.
113
+
77
114
  ```js
78
115
  import { PlayerAnalytics } from "@eyevinn/player-analytics-client-sdk-web";
79
116
 
@@ -81,9 +118,6 @@ import { PlayerAnalytics } from "@eyevinn/player-analytics-client-sdk-web";
81
118
  const playerAnalytics = new PlayerAnalytics("https://your-eventsink-url.io");
82
119
  await playerAnalytics.initiateAnalyticsReporter({
83
120
  sessionId: "generated-unique-uuid-session-id",
84
- contentId: "big-buck-bunny-720",
85
- contentUrl:
86
- "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4",
87
121
  });
88
122
 
89
123
  // then trigger the method calls accordingly, e.g.
@@ -92,8 +126,8 @@ videoElement.addEventListener("play", () => {
92
126
  playerAnalytics.playing({
93
127
  event: "playing",
94
128
  timestamp: Date.now(),
95
- playhead: 0,
96
- duration: 3600,
129
+ playhead: videoElement.currentTime,
130
+ duration: videoElement.duration,
97
131
  sessionId: "generated-unique-uuid-session-id",
98
132
  });
99
133
  });
@@ -101,63 +135,214 @@ videoElement.addEventListener("play", () => {
101
135
 
102
136
  ### Report Metadata
103
137
 
104
- You can report your metadata at a later stage in your code and tailor ther callbacks for your tech stach, for example when you have parsed your manifest.
105
- 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`:
106
140
 
107
141
  ```js
108
142
  import Hls from "hls.js";
109
143
  import { PlayerAnalyticsConnector } from "@eyevinn/player-analytics-client-sdk-web";
110
144
 
111
- // Create your instance and set the analytics eventsink endpoint
112
- const playerAnalytics = new PlayerAnalytics("https://your-eventsink-url.io");
113
- await playerAnalytics.initiateAnalyticsReporter({
145
+ const analytics = new PlayerAnalyticsConnector("https://your-eventsink-url.io");
146
+ await analytics.init({
114
147
  sessionId: "generated-unique-uuid-session-id",
115
148
  });
116
149
 
117
150
  const hls = new Hls();
118
151
  const videoElement = document.querySelector("video");
119
152
 
153
+ analytics.load(videoElement);
120
154
  hls.attachMedia(videoElement);
121
- hls.load(src);
155
+ hls.loadSource(src);
122
156
  hls.on(Hls.Events.LEVEL_LOADED, (event, data) => {
123
- playerAnalytics.metadata({
124
- live: data?.details?.level?.live,
157
+ analytics.reportMetadata({
158
+ live: data?.details?.live,
159
+ contentTitle: "My Video",
160
+ contentUrl: src,
125
161
  });
126
162
  });
127
163
  ```
128
164
 
129
- These are the available keys for metadata, which can be sent anytime between a `init` and a `stopped` call.
165
+ These are the available keys for metadata, which can be sent anytime between an `init` and a `stopped` call.
130
166
 
131
167
  ```ts
132
- export interface TMetadataEventPayload {
133
- live?: boolean;
134
- contentTitle?: string;
135
- contentId?: string;
136
- contentUrl?: string;
137
- drmType?: string;
138
- userId?: string;
139
- deviceId?: string;
140
- deviceModel?: string;
141
- deviceType?: string;
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
142
179
  }
143
180
  ```
144
181
 
145
- ### Constructor parameters
182
+ ### Events Tracked
146
183
 
147
- These applies to both the `PlayerAnalyticsConnector` and `PlayerAnalytics`.
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:
148
185
 
149
- - `eventsinkUrl`, the url to your event sink.
150
- - `debug`, default false, triggers output to dev console rather than actual http posts.
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"`) |
151
198
 
152
- ### Init parameters
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).
200
+
201
+ ### Unload-time delivery (page close / reload)
202
+
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
153
220
 
154
221
  ```ts
155
- export interface IPlayerAnalyticsInitOptions {
156
- sessionId?: string; // should be generated by the backend if not sent in
157
- 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)
158
226
  }
159
227
  ```
160
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
+
161
346
  ## Development
162
347
 
163
348
  Run the demo page by `npm start`
@@ -175,3 +360,20 @@ Run the demo page by `npm start`
175
360
  - click "Auto-generate release notes"
176
361
  - click "Publish release"
177
362
  - A Github Action is triggered to do the releases towards the npmjs and github packages repositories.
363
+ <br/>
364
+ <br/>
365
+
366
+ # About Eyevinn
367
+ We are [Eyevinn Technology](https://www.eyevinntechnology.se/), and we help companies in the TV, media, and entertainment sectors optimize costs and boost profitability through enhanced media solutions. We are independent in a way that we are not commercially tied to any platform or technology vendor. As our way to innovate and push the industry forward, we develop proof-of-concepts and tools. We share things we have learn and code as open-source.
368
+
369
+ With Eyevinn Open Source Cloud we enable to build solutions and applications based on Open Web Services and avoid being locked in with a single web service vendor. Our open-source solutions offer full flexibility with a revenue share model that supports the creators.
370
+
371
+ Read our blogs and articles here:
372
+
373
+ - [Developer blogs](https://dev.to/video)
374
+ - [Medium](https://eyevinntechnology.medium.com/)
375
+ - [OSC](https://blog.osaas.io)
376
+ - [LinkedIn](https://www.linkedin.com/company/eyevinn/)
377
+
378
+ Want to know more about Eyevinn, contact us at [info@eyevinn.se](mailto:info@eyevinn.se)!
379
+
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";