@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 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 playerAnalytics = new PlayerAnalyticsConnector(
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 you session in the backend
34
+ // This will create your session in the backend
35
35
  try {
36
- await playerAnalytics.init({
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
- playerAnalytics.load(videoElement);
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
- playerAnalytics.deinit();
47
+ analytics.deinit();
48
48
  }
49
49
  ```
50
50
 
51
- 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.
52
75
 
53
76
  ```js
54
- playerAnalytics.reportBitrateChange({
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 an end event as well
65
- playerAnalytics.reportError({
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
- playerAnalytics.reportWarning({
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 manor
83
- playerAnalytics.reportStop();
84
- playerAnalytics.destroy();
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: 0,
108
- duration: 3600,
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 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.
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
- // Create your instance and set the analytics eventsink endpoint
124
- const playerAnalytics = new PlayerAnalytics("https://your-eventsink-url.io");
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.load(src);
155
+ hls.loadSource(src);
134
156
  hls.on(Hls.Events.LEVEL_LOADED, (event, data) => {
135
- playerAnalytics.metadata({
136
- live: data?.details?.level?.live,
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 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.
142
166
 
143
167
  ```ts
144
- export interface TMetadataEventPayload {
145
- live?: boolean;
146
- contentTitle?: string;
147
- contentId?: string;
148
- contentUrl?: string;
149
- drmType?: string;
150
- userId?: string;
151
- deviceId?: string;
152
- deviceModel?: string;
153
- 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
154
179
  }
155
180
  ```
156
181
 
157
- ### Constructor parameters
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
- These applies to both the `PlayerAnalyticsConnector` and `PlayerAnalytics`.
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
- - `eventsinkUrl`, the url to your event sink.
162
- - `debug`, default false, triggers output to dev console rather than actual http posts.
201
+ ### Unload-time delivery (page close / reload)
163
202
 
164
- ### Init parameters
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 IPlayerAnalyticsInitOptions {
168
- sessionId?: string; // should be generated by the backend if not sent in
169
- shardId?: string; // optional grouping or clustering events throughout the
170
- // pipeline to allow sharding at various stages of a pipeline
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";