@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.
package/dist/index.js CHANGED
@@ -156,44 +156,81 @@ var $4cb1735bdc43624b$var$__generator = undefined && undefined.__generator || fu
156
156
  return this;
157
157
  }), g;
158
158
  };
159
+ var $4cb1735bdc43624b$var$MAX_QUEUE_SIZE = 100;
159
160
  var $4cb1735bdc43624b$export$957313a2f485e5ed = function() {
160
161
  function $4cb1735bdc43624b$export$957313a2f485e5ed(options) {
161
- this.isInitiated = false;
162
- this.debug = options.debug;
162
+ var _a;
163
+ this.state = "idle";
164
+ this.eventQueue = [];
165
+ this.initPromise = null;
166
+ this.debug = (_a = options.debug) !== null && _a !== void 0 ? _a : false;
167
+ this.shardId = options.shardId;
163
168
  this.eventsinkUrl = options.eventsinkUrl;
164
169
  this.sessionId = options.sessionId;
165
170
  this.heartbeatInterval = options.heartbeatInterval || $e8f16a459684b36f$export$31c453b5fe09788d;
171
+ this.onError = options.onError;
166
172
  if (this.debug) console.log("[AnalyticsReporter] Initiated AnalyticsReporter", options);
167
173
  }
174
+ Object.defineProperty($4cb1735bdc43624b$export$957313a2f485e5ed.prototype, "isInitiated", {
175
+ get: function() {
176
+ return this.state === "ready";
177
+ },
178
+ enumerable: false,
179
+ configurable: true
180
+ });
168
181
  $4cb1735bdc43624b$export$957313a2f485e5ed.prototype.init = function(sessionId) {
169
182
  return $4cb1735bdc43624b$var$__awaiter(this, void 0, Promise, function() {
170
- var data, initResponse, initResponseJson;
183
+ var data, result;
184
+ return $4cb1735bdc43624b$var$__generator(this, function(_a) {
185
+ if (this.state !== "idle") return [
186
+ 2 /*return*/ ,
187
+ this.initPromise
188
+ ];
189
+ this.sessionId = sessionId || this.sessionId;
190
+ this.state = "initializing";
191
+ data = {
192
+ event: "init",
193
+ sessionId: this.sessionId,
194
+ shardId: this.shardId,
195
+ timestamp: Date.now(),
196
+ playhead: -1,
197
+ duration: -1
198
+ };
199
+ if (this.debug) {
200
+ console.log("[AnalyticsReporter] Init session:", data);
201
+ this.state = "ready";
202
+ this.flushQueue();
203
+ result = {
204
+ sessionId: this.sessionId,
205
+ heartbeatInterval: this.heartbeatInterval,
206
+ isInitiated: true
207
+ };
208
+ this.initPromise = Promise.resolve(result);
209
+ return [
210
+ 2 /*return*/ ,
211
+ this.initPromise
212
+ ];
213
+ }
214
+ this.initPromise = this.doInit(data);
215
+ return [
216
+ 2 /*return*/ ,
217
+ this.initPromise
218
+ ];
219
+ });
220
+ });
221
+ };
222
+ $4cb1735bdc43624b$export$957313a2f485e5ed.prototype.doInit = function(data) {
223
+ return $4cb1735bdc43624b$var$__awaiter(this, void 0, Promise, function() {
224
+ var initResponse, initResponseJson, err_1;
171
225
  return $4cb1735bdc43624b$var$__generator(this, function(_a) {
172
226
  switch(_a.label){
173
227
  case 0:
174
- this.sessionId = sessionId || this.sessionId;
175
- data = {
176
- event: "init",
177
- sessionId: this.sessionId,
178
- timestamp: Date.now(),
179
- playhead: -1,
180
- duration: -1
181
- };
182
- if (!this.debug) return [
183
- 3 /*break*/ ,
184
- 1
185
- ];
186
- console.log("[AnalyticsReporter] Init session:", data);
187
- this.isInitiated = true;
188
- return [
189
- 2 /*return*/ ,
190
- {
191
- sessionId: this.sessionId,
192
- heartbeatInterval: this.heartbeatInterval,
193
- isInitiated: this.isInitiated
194
- }
195
- ];
196
- case 1:
228
+ _a.trys.push([
229
+ 0,
230
+ 6,
231
+ ,
232
+ 7
233
+ ]);
197
234
  return [
198
235
  4 /*yield*/ ,
199
236
  fetch("".concat(this.eventsinkUrl), {
@@ -208,37 +245,150 @@ var $4cb1735bdc43624b$export$957313a2f485e5ed = function() {
208
245
  body: JSON.stringify(data)
209
246
  })
210
247
  ];
211
- case 2:
248
+ case 1:
212
249
  initResponse = _a.sent();
250
+ // Abort if destroy() was called during the await
251
+ if (this.state === "destroyed") throw new Error("[AnalyticsReporter] init aborted: reporter was destroyed");
213
252
  if (!initResponse.ok) throw new Error("[AnalyticsReporter] init failed: ".concat(initResponse.statusText));
214
253
  return [
215
254
  4 /*yield*/ ,
216
255
  initResponse.json()
217
256
  ];
218
- case 3:
257
+ case 2:
219
258
  initResponseJson = _a.sent();
259
+ if (this.state === "destroyed") throw new Error("[AnalyticsReporter] init aborted: reporter was destroyed");
220
260
  if (!this.sessionId && !initResponseJson.sessionId) throw new Error("[AnalyticsReporter] init failed: no sessionId");
221
261
  if (initResponseJson.sessionId) this.sessionId = initResponseJson.sessionId;
222
- this.isInitiated = true;
262
+ _a.label = 3;
263
+ case 3:
264
+ if (!(this.eventQueue.length > 0)) return [
265
+ 3 /*break*/ ,
266
+ 5
267
+ ];
268
+ if (this.state === "destroyed") throw new Error("[AnalyticsReporter] init aborted: reporter was destroyed");
269
+ return [
270
+ 4 /*yield*/ ,
271
+ this.flushQueue()
272
+ ];
273
+ case 4:
274
+ _a.sent();
275
+ return [
276
+ 3 /*break*/ ,
277
+ 3
278
+ ];
279
+ case 5:
280
+ // Don't transition to ready if destroyed during flush
281
+ if (this.state !== "destroyed") this.state = "ready";
223
282
  return [
224
283
  2 /*return*/ ,
225
284
  {
226
285
  heartbeatInterval: this.heartbeatInterval,
227
286
  sessionId: this.sessionId,
228
- isInitiated: this.isInitiated
287
+ isInitiated: true
229
288
  }
230
289
  ];
290
+ case 6:
291
+ err_1 = _a.sent();
292
+ // Don't overwrite 'destroyed' state with 'failed'
293
+ if (this.state !== "destroyed") this.state = "failed";
294
+ this.eventQueue = [];
295
+ throw err_1;
296
+ case 7:
297
+ return [
298
+ 2 /*return*/
299
+ ];
231
300
  }
232
301
  });
233
302
  });
234
303
  };
235
304
  $4cb1735bdc43624b$export$957313a2f485e5ed.prototype.send = function(data) {
236
- if (!this.isInitiated) return console.warn("[AnalyticsReporter] Cannot report before initiation:", data);
237
- var payload = $4cb1735bdc43624b$var$__assign({
238
- sessionId: this.sessionId
239
- }, data);
240
- if (this.debug) console.log("[AnalyticsReporter] Send payload:", payload);
241
- else fetch("".concat(this.eventsinkUrl), {
305
+ switch(this.state){
306
+ case "ready":
307
+ this.dispatch(data);
308
+ break;
309
+ case "initializing":
310
+ if (this.eventQueue.length >= $4cb1735bdc43624b$var$MAX_QUEUE_SIZE) {
311
+ console.warn("[AnalyticsReporter] Event queue full, dropping newest event");
312
+ break;
313
+ }
314
+ this.eventQueue.push(data);
315
+ break;
316
+ case "idle":
317
+ console.warn("[AnalyticsReporter] Cannot report before initiation:", data);
318
+ break;
319
+ case "failed":
320
+ console.warn("[AnalyticsReporter] Init failed, cannot send:", data);
321
+ break;
322
+ case "destroyed":
323
+ break;
324
+ }
325
+ };
326
+ $4cb1735bdc43624b$export$957313a2f485e5ed.prototype.buildPayload = function(data) {
327
+ return $4cb1735bdc43624b$var$__assign($4cb1735bdc43624b$var$__assign({
328
+ }, data), {
329
+ sessionId: this.sessionId,
330
+ shardId: this.shardId
331
+ });
332
+ };
333
+ /**
334
+ * Deliver an event using a transport that survives page unload (tab close
335
+ * or reload).
336
+ *
337
+ * Uses `navigator.sendBeacon` with a `text/plain` JSON body. A plain-text
338
+ * body with no `X-EPAS-*` headers is a CORS "simple request", so the browser
339
+ * does not issue a preflight — the preflight is what gets dropped while the
340
+ * page is unloading, losing the event (notably the `stopped` event). The
341
+ * eventsink reads the event from the JSON body (`body.event`), not from
342
+ * headers, so the beacon is accepted.
343
+ *
344
+ * Falls back to `fetch(..., { keepalive: true })` when `sendBeacon` is
345
+ * unavailable or refuses the payload. Returns true when the event was handed
346
+ * off to a transport.
347
+ */ $4cb1735bdc43624b$export$957313a2f485e5ed.prototype.sendBeacon = function(data) {
348
+ if (this.state === "destroyed") return false;
349
+ var payload = this.buildPayload(data);
350
+ if (this.debug) {
351
+ console.log("[AnalyticsReporter] Send beacon payload:", payload);
352
+ return true;
353
+ }
354
+ var body = JSON.stringify(payload);
355
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") try {
356
+ var blob = new Blob([
357
+ body
358
+ ], {
359
+ type: "text/plain; charset=utf-8"
360
+ });
361
+ if (navigator.sendBeacon(this.eventsinkUrl, blob)) return true;
362
+ } catch (_a) {
363
+ // Fall through to the keepalive fetch fallback below.
364
+ }
365
+ if (typeof fetch === "function") try {
366
+ fetch("".concat(this.eventsinkUrl), {
367
+ method: "POST",
368
+ mode: "cors",
369
+ cache: "no-cache",
370
+ keepalive: true,
371
+ headers: {
372
+ "Content-Type": "text/plain; charset=utf-8"
373
+ },
374
+ body: body
375
+ }).catch(function() {
376
+ // Best-effort during unload — nothing actionable to do on failure.
377
+ });
378
+ return true;
379
+ } catch (_b) {
380
+ return false;
381
+ }
382
+ return false;
383
+ };
384
+ $4cb1735bdc43624b$export$957313a2f485e5ed.prototype.dispatch = function(data) {
385
+ var _this = this;
386
+ var payload = this.buildPayload(data);
387
+ if (this.debug) {
388
+ console.log("[AnalyticsReporter] Send payload:", payload);
389
+ return Promise.resolve();
390
+ }
391
+ return fetch("".concat(this.eventsinkUrl), {
242
392
  method: "POST",
243
393
  mode: "cors",
244
394
  cache: "no-cache",
@@ -248,8 +398,73 @@ var $4cb1735bdc43624b$export$957313a2f485e5ed = function() {
248
398
  "X-EPAS-Version": $e8f16a459684b36f$export$8a65ed80196068d4
249
399
  },
250
400
  body: JSON.stringify(payload)
401
+ }).then(function(res) {
402
+ if (res && !res.ok) {
403
+ var error = {
404
+ status: res.status,
405
+ statusText: res.statusText,
406
+ message: "Send failed: ".concat(res.status, " ").concat(res.statusText)
407
+ };
408
+ if (_this.onError) _this.onError(error, data);
409
+ else console.warn("[AnalyticsReporter] ".concat(error.message));
410
+ }
411
+ }).catch(function(err) {
412
+ var error = {
413
+ message: err instanceof Error ? err.message : String(err)
414
+ };
415
+ if (_this.onError) _this.onError(error, data);
416
+ else console.warn("[AnalyticsReporter] Send failed:", error.message);
251
417
  });
252
418
  };
419
+ $4cb1735bdc43624b$export$957313a2f485e5ed.prototype.flushQueue = function() {
420
+ return $4cb1735bdc43624b$var$__awaiter(this, void 0, Promise, function() {
421
+ var queued, _i, queued_1, event;
422
+ return $4cb1735bdc43624b$var$__generator(this, function(_a) {
423
+ switch(_a.label){
424
+ case 0:
425
+ queued = this.eventQueue;
426
+ this.eventQueue = [];
427
+ _i = 0, queued_1 = queued;
428
+ _a.label = 1;
429
+ case 1:
430
+ if (!(_i < queued_1.length)) return [
431
+ 3 /*break*/ ,
432
+ 4
433
+ ];
434
+ event = queued_1[_i];
435
+ // Abort if destroy() was called mid-flush
436
+ if (this.state === "destroyed") return [
437
+ 2 /*return*/
438
+ ];
439
+ return [
440
+ 4 /*yield*/ ,
441
+ this.dispatch(event)
442
+ ];
443
+ case 2:
444
+ _a.sent();
445
+ _a.label = 3;
446
+ case 3:
447
+ _i++;
448
+ return [
449
+ 3 /*break*/ ,
450
+ 1
451
+ ];
452
+ case 4:
453
+ return [
454
+ 2 /*return*/
455
+ ];
456
+ }
457
+ });
458
+ });
459
+ };
460
+ /**
461
+ * Tear down the reporter. Aborts any in-flight init, discards queued
462
+ * events without flushing, and prevents future send() calls from
463
+ * dispatching. Called by PlayerAnalytics.destroy().
464
+ */ $4cb1735bdc43624b$export$957313a2f485e5ed.prototype.destroy = function() {
465
+ this.state = "destroyed";
466
+ this.eventQueue = [];
467
+ };
253
468
  return $4cb1735bdc43624b$export$957313a2f485e5ed;
254
469
  }();
255
470
 
@@ -377,13 +592,14 @@ var $7d88e8cdd46a09a5$var$__generator = undefined && undefined.__generator || fu
377
592
  }), g;
378
593
  };
379
594
  var $7d88e8cdd46a09a5$export$f76c463fe9cb9f15 = function() {
380
- function $7d88e8cdd46a09a5$export$f76c463fe9cb9f15(eventsinkUrl, debug) {
595
+ function $7d88e8cdd46a09a5$export$f76c463fe9cb9f15(eventsinkUrl, debug, onError) {
381
596
  this.debug = false;
382
- this.debug = debug;
597
+ this.debug = debug !== null && debug !== void 0 ? debug : false;
383
598
  this.eventsinkUrl = eventsinkUrl;
599
+ this.onError = onError;
384
600
  }
385
601
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.initiateAnalyticsReporter = function(_a) {
386
- var sessionId = _a.sessionId, _b = _a.heartbeatInterval, heartbeatInterval = _b === void 0 ? $e8f16a459684b36f$export$31c453b5fe09788d : _b;
602
+ var sessionId = _a.sessionId, shardId = _a.shardId, _b = _a.heartbeatInterval, heartbeatInterval = _b === void 0 ? $e8f16a459684b36f$export$31c453b5fe09788d : _b;
387
603
  return $7d88e8cdd46a09a5$var$__awaiter(this, void 0, void 0, function() {
388
604
  var _c, generatedSessionId, isInitiated;
389
605
  return $7d88e8cdd46a09a5$var$__generator(this, function(_d) {
@@ -393,7 +609,9 @@ var $7d88e8cdd46a09a5$export$f76c463fe9cb9f15 = function() {
393
609
  sessionId: sessionId,
394
610
  eventsinkUrl: this.eventsinkUrl,
395
611
  debug: this.debug,
396
- heartbeatInterval: heartbeatInterval
612
+ heartbeatInterval: heartbeatInterval,
613
+ shardId: shardId,
614
+ onError: this.onError
397
615
  });
398
616
  return [
399
617
  4 /*yield*/ ,
@@ -413,52 +631,87 @@ var $7d88e8cdd46a09a5$export$f76c463fe9cb9f15 = function() {
413
631
  });
414
632
  });
415
633
  };
634
+ $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.ensureReporter = function() {
635
+ if (!this.analyticsReporter) {
636
+ console.warn("[PlayerAnalytics] Not initialized. Call initiateAnalyticsReporter() first.");
637
+ return false;
638
+ }
639
+ return true;
640
+ };
416
641
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.init = function(data) {
642
+ if (!this.ensureReporter()) return;
417
643
  this.analyticsReporter.send(data);
418
644
  };
419
645
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.metadata = function(data) {
646
+ if (!this.ensureReporter()) return;
420
647
  this.analyticsReporter.send(data);
421
648
  };
422
649
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.heartbeat = function(data) {
650
+ if (!this.ensureReporter()) return;
423
651
  this.analyticsReporter.send(data);
424
652
  };
425
653
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.loading = function(data) {
654
+ if (!this.ensureReporter()) return;
426
655
  this.analyticsReporter.send(data);
427
656
  };
428
657
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.loaded = function(data) {
658
+ if (!this.ensureReporter()) return;
429
659
  this.analyticsReporter.send(data);
430
660
  };
431
661
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.playing = function(data) {
662
+ if (!this.ensureReporter()) return;
432
663
  this.analyticsReporter.send(data);
433
664
  };
434
665
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.pause = function(data) {
666
+ if (!this.ensureReporter()) return;
435
667
  this.analyticsReporter.send(data);
436
668
  };
437
669
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.buffering = function(data) {
670
+ if (!this.ensureReporter()) return;
438
671
  this.analyticsReporter.send(data);
439
672
  };
440
673
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.buffered = function(data) {
674
+ if (!this.ensureReporter()) return;
441
675
  this.analyticsReporter.send(data);
442
676
  };
443
677
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.seeking = function(data) {
678
+ if (!this.ensureReporter()) return;
444
679
  this.analyticsReporter.send(data);
445
680
  };
446
681
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.seeked = function(data) {
682
+ if (!this.ensureReporter()) return;
447
683
  this.analyticsReporter.send(data);
448
684
  };
449
685
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.bitrateChanged = function(data) {
686
+ if (!this.ensureReporter()) return;
450
687
  this.analyticsReporter.send(data);
451
688
  };
452
689
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.error = function(data) {
690
+ if (!this.ensureReporter()) return;
453
691
  this.analyticsReporter.send(data);
454
692
  };
455
693
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.warning = function(data) {
694
+ if (!this.ensureReporter()) return;
456
695
  this.analyticsReporter.send(data);
457
696
  };
458
697
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.stopped = function(data) {
698
+ if (!this.ensureReporter()) return;
459
699
  this.analyticsReporter.send(data);
460
700
  };
701
+ /**
702
+ * Send a stopped event over the reporter's unload-safe beacon transport
703
+ * (navigator.sendBeacon with a fetch keepalive fallback). Used to deliver the
704
+ * stopped event when the page is closed or reloaded mid-playback, where a
705
+ * normal CORS fetch would be dropped during unload. Returns true when the
706
+ * event was handed off to a transport.
707
+ */ $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.stoppedViaBeacon = function(data) {
708
+ if (!this.ensureReporter()) return false;
709
+ return this.analyticsReporter.sendBeacon(data);
710
+ };
461
711
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.destroy = function() {
712
+ // Cascade destroy to the reporter so any in-flight init is aborted
713
+ // and queued events are discarded rather than flushed.
714
+ if (this.analyticsReporter) this.analyticsReporter.destroy();
462
715
  this.analyticsReporter = null;
463
716
  };
464
717
  return $7d88e8cdd46a09a5$export$f76c463fe9cb9f15;
@@ -603,32 +856,83 @@ var $c2e08ff125b5bcd1$var$__generator = undefined && undefined.__generator || fu
603
856
  }), g;
604
857
  };
605
858
  var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
606
- function $c2e08ff125b5bcd1$export$2daf9923c014a26e(eventsinkUrl, debug) {
859
+ function $c2e08ff125b5bcd1$export$2daf9923c014a26e(eventsinkUrl, debug, onError) {
860
+ var _this = this;
607
861
  this.analyticsInitiated = false;
862
+ this.initCalled = false;
863
+ this.initGeneration = 0;
864
+ // Direct DOM fallback for terminal/seek events on an element that was already
865
+ // playing when load() ran (autoplay started during a slow init() handshake).
866
+ // The media-event-filter attaches in its "loading" state and, never having
867
+ // observed the initial playing/canplaythrough transitions, stays stuck there
868
+ // and suppresses ENDED/SEEKING/SEEKED. These listeners drive that path
869
+ // directly. They self-disable the moment the filter proves it is live (emits
870
+ // any event), so a seek is never reported twice across the two sources.
871
+ this.autoplayFallbackActive = false;
872
+ this.pendingHeartbeatStart = false;
873
+ // Tracks whether a stopped event has already been reported for the current
874
+ // session, so the page-unload path does not emit a duplicate after an
875
+ // `ended`, reportStop(), or reportError() has already stopped the session.
876
+ this.stoppedReported = false;
877
+ this.unloadListenersRegistered = false;
878
+ this.handleAutoplayEnded = function() {
879
+ // Mirror the filter's ENDED branch: exactly one stopped(ended), stop
880
+ // heartbeats, and respect the stoppedReported guard so reportStop/
881
+ // reportError/unload can't produce a duplicate.
882
+ if (_this.stoppedReported) return;
883
+ _this.stoppedReported = true;
884
+ _this.stopInterval();
885
+ _this.sendFilteredEvent("stopped", {
886
+ reason: "ended"
887
+ });
888
+ };
889
+ this.handleAutoplaySeeking = function() {
890
+ _this.sendFilteredEvent("seeking");
891
+ };
892
+ this.handleAutoplaySeeked = function() {
893
+ _this.sendFilteredEvent("seeked");
894
+ };
895
+ this.handlePageHide = function() {
896
+ _this.flushStoppedOnUnload();
897
+ };
608
898
  this.eventsinkUrl = eventsinkUrl;
609
- this.playerAnalytics = new $7d88e8cdd46a09a5$exports.PlayerAnalytics(this.eventsinkUrl, debug);
899
+ this.playerAnalytics = new $7d88e8cdd46a09a5$exports.PlayerAnalytics(this.eventsinkUrl, debug, onError);
610
900
  }
611
901
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.init = function(options) {
612
902
  return $c2e08ff125b5bcd1$var$__awaiter(this, void 0, void 0, function() {
613
- var _a, heartbeatInterval, isInitiated;
614
- return $c2e08ff125b5bcd1$var$__generator(this, function(_b) {
615
- switch(_b.label){
616
- case 0:
617
- this.sessionId = options.sessionId;
618
- return [
619
- 4 /*yield*/ ,
620
- this.playerAnalytics.initiateAnalyticsReporter($c2e08ff125b5bcd1$var$__assign({
621
- sessionId: this.sessionId
622
- }, options))
623
- ];
624
- case 1:
625
- _a = _b.sent(), heartbeatInterval = _a.heartbeatInterval, isInitiated = _a.isInitiated;
626
- this.analyticsInitiated = isInitiated;
627
- this.heartbeatInterval = heartbeatInterval;
628
- return [
629
- 2 /*return*/
630
- ];
631
- }
903
+ var currentGeneration, initPromise;
904
+ var _this = this;
905
+ return $c2e08ff125b5bcd1$var$__generator(this, function(_a) {
906
+ this.sessionId = options.sessionId;
907
+ this.initCalled = true;
908
+ this.stoppedReported = false;
909
+ currentGeneration = ++this.initGeneration;
910
+ initPromise = this.playerAnalytics.initiateAnalyticsReporter($c2e08ff125b5bcd1$var$__assign($c2e08ff125b5bcd1$var$__assign({
911
+ }, options), {
912
+ sessionId: this.sessionId
913
+ }));
914
+ initPromise.then(function(result) {
915
+ // Ignore stale init completions (e.g., if destroy() was called during init)
916
+ if (currentGeneration !== _this.initGeneration) return;
917
+ _this.analyticsInitiated = result.isInitiated === true;
918
+ _this.heartbeatInterval = Number(result.heartbeatInterval) || _this.heartbeatInterval;
919
+ if (typeof result.sessionId === "string" && result.sessionId) _this.sessionId = result.sessionId;
920
+ if (_this.pendingHeartbeatStart) {
921
+ _this.pendingHeartbeatStart = false;
922
+ _this.startInterval();
923
+ }
924
+ }).catch(function(err) {
925
+ if (currentGeneration !== _this.initGeneration) return;
926
+ // Reset pendingHeartbeatStart so a retry doesn't fire heartbeats
927
+ // from a stale flag without a new PLAYING event.
928
+ _this.pendingHeartbeatStart = false;
929
+ var message = err instanceof Error ? err.message : String(err);
930
+ console.warn("[PlayerAnalyticsConnector] Init failed:", message);
931
+ });
932
+ return [
933
+ 2 /*return*/ ,
934
+ initPromise
935
+ ];
632
936
  });
633
937
  });
634
938
  };
@@ -638,6 +942,30 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
638
942
  event: "loading"
639
943
  }, this.playbackState()));
640
944
  this.initiateVideoEventFilter();
945
+ this.registerUnloadListeners();
946
+ // If the element is already playing by the time load() is called (e.g.
947
+ // autoplay started during a slow init() handshake, before listeners were
948
+ // attached), the media-event-filter won't emit a fresh PLAYING for the
949
+ // DOM "playing" event that already fired. Emit it ourselves and start the
950
+ // heartbeat so the session isn't lost. While init() is still in-flight the
951
+ // event is queued by the Reporter and the heartbeat start is deferred via
952
+ // pendingHeartbeatStart, flushing once the handshake completes.
953
+ if (this.isPlayerAlreadyPlaying()) {
954
+ this.playerAnalytics.playing($c2e08ff125b5bcd1$var$__assign({
955
+ event: "playing"
956
+ }, this.playbackState()));
957
+ this.startInterval();
958
+ // The filter won't emit terminal/seek events for this already-playing
959
+ // element (see isPlayerAlreadyPlaying / initiateVideoEventFilter notes),
960
+ // so drive ended/seeking/seeked from the element's own DOM events.
961
+ this.attachAutoplayTerminalFallback();
962
+ }
963
+ };
964
+ $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.isPlayerAlreadyPlaying = function() {
965
+ // HAVE_FUTURE_DATA (readyState 3) mirrors the media-event-filter's own
966
+ // "ready" threshold. Use the numeric literal rather than
967
+ // HTMLMediaElement.HAVE_FUTURE_DATA so this works in non-DOM test envs.
968
+ return !!(this.player && !this.player.paused && !this.player.ended && this.player.readyState >= 3);
641
969
  };
642
970
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.initiateVideoEventFilter = function() {
643
971
  var _this = this;
@@ -646,6 +974,11 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
646
974
  mediaElement: this.player,
647
975
  mp4Mode: false,
648
976
  callback: function(event) {
977
+ // The filter emitting anything means it has left its initial "loading"
978
+ // state and is now the authoritative source for terminal/seek events;
979
+ // retire the autoplay DOM fallback so a single seek/ended is not
980
+ // reported twice.
981
+ _this.detachAutoplayTerminalFallback();
649
982
  var eventType;
650
983
  var extraData = {
651
984
  };
@@ -673,32 +1006,65 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
673
1006
  eventType = "buffered";
674
1007
  break;
675
1008
  case $g5Y9E$eyevinnmediaeventfilter.FilteredMediaEvent.ENDED:
1009
+ // Guard against a duplicate stopped(ended) if the autoplay fallback
1010
+ // (or reportStop/reportError) already ended the session.
1011
+ if (_this.stoppedReported) return;
676
1012
  eventType = "stopped";
677
1013
  extraData["reason"] = "ended";
1014
+ _this.stoppedReported = true;
678
1015
  _this.stopInterval();
679
1016
  break;
680
1017
  default:
681
1018
  break;
682
1019
  }
683
- try {
684
- if (!_this.analyticsInitiated) {
685
- console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
686
- return;
687
- }
688
- if (eventType) _this.playerAnalytics[eventType == "paused" ? "pause" : eventType]($c2e08ff125b5bcd1$var$__assign($c2e08ff125b5bcd1$var$__assign({
689
- event: eventType
690
- }, _this.playbackState()), Object.keys(extraData).length > 0 && {
691
- payload: extraData
692
- }));
693
- } catch (err) {
694
- console.error(err);
695
- }
1020
+ _this.sendFilteredEvent(eventType, extraData);
696
1021
  }
697
1022
  });
698
1023
  };
1024
+ // Shared emit path for events sourced from the media-event-filter and from
1025
+ // the autoplay DOM fallback, so both behave identically (init guard, the
1026
+ // paused→pause method-name mapping, optional payload, error swallowing).
1027
+ $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.sendFilteredEvent = function(eventType, extraData) {
1028
+ if (extraData === void 0) extraData = {
1029
+ };
1030
+ try {
1031
+ if (!this.initCalled) {
1032
+ console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
1033
+ return;
1034
+ }
1035
+ if (eventType) this.playerAnalytics[eventType == "paused" ? "pause" : eventType]($c2e08ff125b5bcd1$var$__assign($c2e08ff125b5bcd1$var$__assign({
1036
+ event: eventType
1037
+ }, this.playbackState()), Object.keys(extraData).length > 0 && {
1038
+ payload: extraData
1039
+ }));
1040
+ } catch (err) {
1041
+ console.error(err);
1042
+ }
1043
+ };
1044
+ $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.attachAutoplayTerminalFallback = function() {
1045
+ if (this.autoplayFallbackActive) return;
1046
+ if (!this.player || typeof this.player.addEventListener !== "function") return;
1047
+ this.autoplayFallbackActive = true;
1048
+ this.player.addEventListener("ended", this.handleAutoplayEnded);
1049
+ this.player.addEventListener("seeking", this.handleAutoplaySeeking);
1050
+ this.player.addEventListener("seeked", this.handleAutoplaySeeked);
1051
+ };
1052
+ $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.detachAutoplayTerminalFallback = function() {
1053
+ if (!this.autoplayFallbackActive) return;
1054
+ this.autoplayFallbackActive = false;
1055
+ if (this.player && typeof this.player.removeEventListener === "function") {
1056
+ this.player.removeEventListener("ended", this.handleAutoplayEnded);
1057
+ this.player.removeEventListener("seeking", this.handleAutoplaySeeking);
1058
+ this.player.removeEventListener("seeked", this.handleAutoplaySeeked);
1059
+ }
1060
+ };
699
1061
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.startInterval = function() {
700
1062
  var _this = this;
701
1063
  if (this.heartbeatIntervalTimer) return;
1064
+ if (!this.heartbeatInterval) {
1065
+ this.pendingHeartbeatStart = true;
1066
+ return;
1067
+ }
702
1068
  this.heartbeatIntervalTimer = setInterval(function() {
703
1069
  _this.playerAnalytics.heartbeat($c2e08ff125b5bcd1$var$__assign({
704
1070
  event: "heartbeat"
@@ -707,9 +1073,11 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
707
1073
  };
708
1074
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.stopInterval = function() {
709
1075
  clearInterval(this.heartbeatIntervalTimer);
1076
+ this.heartbeatIntervalTimer = null;
1077
+ this.pendingHeartbeatStart = false;
710
1078
  };
711
1079
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.reportBitrateChange = function(payload) {
712
- if (!this.analyticsInitiated) {
1080
+ if (!this.initCalled) {
713
1081
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
714
1082
  return;
715
1083
  }
@@ -720,21 +1088,32 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
720
1088
  }));
721
1089
  };
722
1090
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.reportStop = function() {
723
- if (!this.analyticsInitiated) {
1091
+ if (!this.initCalled) {
724
1092
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
725
1093
  return;
726
1094
  }
727
- this.playerAnalytics.stopped($c2e08ff125b5bcd1$var$__assign($c2e08ff125b5bcd1$var$__assign({
1095
+ // Already stopped this session (an `ended`, reportError(), or a prior
1096
+ // unload beacon) — don't emit a duplicate stopped.
1097
+ if (this.stoppedReported) return;
1098
+ this.stoppedReported = true;
1099
+ this.stopInterval();
1100
+ // Deliver over the unload-safe beacon transport (sendBeacon / keepalive
1101
+ // fetch), not the normal CORS fetch. Integrators call reportStop() from
1102
+ // pagehide/unload handlers, where the browser drops the preflighted CORS
1103
+ // fetch and the event is lost. Routing through the same beacon the SDK's
1104
+ // own unload path uses means setting `stoppedReported` here lets
1105
+ // flushStoppedOnUnload() short-circuit without losing the event, so
1106
+ // exactly one stopped is delivered.
1107
+ this.playerAnalytics.stoppedViaBeacon($c2e08ff125b5bcd1$var$__assign($c2e08ff125b5bcd1$var$__assign({
728
1108
  event: "stopped"
729
1109
  }, this.playbackState()), {
730
1110
  payload: {
731
1111
  reason: "aborted"
732
1112
  }
733
1113
  }));
734
- this.stopInterval();
735
1114
  };
736
1115
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.reportError = function(error) {
737
- if (!this.analyticsInitiated) {
1116
+ if (!this.initCalled) {
738
1117
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
739
1118
  return;
740
1119
  }
@@ -750,10 +1129,11 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
750
1129
  reason: "error"
751
1130
  }
752
1131
  }));
1132
+ this.stoppedReported = true;
753
1133
  this.stopInterval();
754
1134
  };
755
1135
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.reportMetadata = function(payload) {
756
- if (!this.analyticsInitiated) {
1136
+ if (!this.initCalled) {
757
1137
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
758
1138
  return;
759
1139
  }
@@ -764,7 +1144,7 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
764
1144
  }));
765
1145
  };
766
1146
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.reportWarning = function(payload) {
767
- if (!this.analyticsInitiated) {
1147
+ if (!this.initCalled) {
768
1148
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
769
1149
  return;
770
1150
  }
@@ -777,7 +1157,7 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
777
1157
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.playbackState = function() {
778
1158
  var _a, _b, _c, _d, _e;
779
1159
  var duration = ((_a = this.player) === null || _a === void 0 ? void 0 : _a.duration) && ((_b = this.player) === null || _b === void 0 ? void 0 : _b.duration) !== Infinity && ((_c = this.player) === null || _c === void 0 ? void 0 : _c.duration) > 0 ? this.player.duration : -1;
780
- var playhead = ((_d = this.player) === null || _d === void 0 ? void 0 : _d.currentTime) && duration !== -1 ? (_e = this.player) === null || _e === void 0 ? void 0 : _e.currentTime : -1;
1160
+ var playhead = ((_d = this.player) === null || _d === void 0 ? void 0 : _d.currentTime) != null && this.player.currentTime >= 0 ? (_e = this.player) === null || _e === void 0 ? void 0 : _e.currentTime : -1;
781
1161
  return {
782
1162
  sessionId: this.sessionId,
783
1163
  timestamp: Date.now(),
@@ -785,25 +1165,73 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
785
1165
  duration: duration
786
1166
  };
787
1167
  };
1168
+ /**
1169
+ * Deliver a stopped event when the page is being unloaded (tab closed or
1170
+ * reloaded) mid-session. Driven by `pagehide`, which fires on genuine
1171
+ * discard (close/reload/navigate-away) but NOT on an ordinary tab switch —
1172
+ * the browser drops the normal CORS fetch (it needs a preflight) during
1173
+ * unload, losing the event, so the stopped event goes out over the
1174
+ * reporter's beacon transport instead. It is sent only once per session (a
1175
+ * later `ended`, reportStop(), or reportError() will already have reported
1176
+ * stopped). `visibilitychange→hidden` is deliberately NOT used here: a tab
1177
+ * switch / backgrounding is not an unload and must not end a still-playing
1178
+ * session (see #44).
1179
+ */ $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.flushStoppedOnUnload = function() {
1180
+ if (!this.initCalled || this.stoppedReported || !this.player) return;
1181
+ this.stoppedReported = true;
1182
+ this.stopInterval();
1183
+ this.playerAnalytics.stoppedViaBeacon($c2e08ff125b5bcd1$var$__assign($c2e08ff125b5bcd1$var$__assign({
1184
+ event: "stopped"
1185
+ }, this.playbackState()), {
1186
+ payload: {
1187
+ reason: "aborted"
1188
+ }
1189
+ }));
1190
+ };
1191
+ $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.registerUnloadListeners = function() {
1192
+ if (this.unloadListenersRegistered) return;
1193
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") window.addEventListener("pagehide", this.handlePageHide);
1194
+ this.unloadListenersRegistered = true;
1195
+ };
1196
+ $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.removeUnloadListeners = function() {
1197
+ if (!this.unloadListenersRegistered) return;
1198
+ if (typeof window !== "undefined" && typeof window.removeEventListener === "function") window.removeEventListener("pagehide", this.handlePageHide);
1199
+ this.unloadListenersRegistered = false;
1200
+ };
788
1201
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.deinit = function() {
789
- if (!this.analyticsInitiated) {
1202
+ if (!this.initCalled) {
790
1203
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
791
1204
  return;
792
1205
  }
1206
+ this.initGeneration++; // Invalidate any pending init callbacks
1207
+ // Abort any in-flight init so queued events don't flush after teardown.
1208
+ // The next init() call creates a fresh Reporter, so destroying the
1209
+ // current one is safe even though deinit is meant to be reusable.
1210
+ this.playerAnalytics.destroy();
793
1211
  this.stopInterval();
1212
+ this.removeUnloadListeners();
1213
+ this.detachAutoplayTerminalFallback();
794
1214
  this.heartbeatInterval = null;
795
1215
  this.videoEventFilter && this.videoEventFilter.teardown();
796
1216
  this.videoEventFilter = null;
1217
+ this.analyticsInitiated = false;
1218
+ this.initCalled = false;
797
1219
  };
798
1220
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.destroy = function() {
799
- if (!this.analyticsInitiated) {
1221
+ if (!this.initCalled) {
800
1222
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
801
1223
  return;
802
1224
  }
1225
+ this.initGeneration++; // Invalidate any pending init callbacks
1226
+ this.stopInterval();
803
1227
  this.playerAnalytics.destroy();
1228
+ this.removeUnloadListeners();
1229
+ this.detachAutoplayTerminalFallback();
804
1230
  this.heartbeatInterval = null;
805
1231
  this.videoEventFilter && this.videoEventFilter.teardown();
806
- this.stopInterval();
1232
+ this.videoEventFilter = null;
1233
+ this.analyticsInitiated = false;
1234
+ this.initCalled = false;
807
1235
  };
808
1236
  return $c2e08ff125b5bcd1$export$2daf9923c014a26e;
809
1237
  }();