@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/dist/index.js CHANGED
@@ -156,46 +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;
163
167
  this.shardId = options.shardId;
164
168
  this.eventsinkUrl = options.eventsinkUrl;
165
169
  this.sessionId = options.sessionId;
166
170
  this.heartbeatInterval = options.heartbeatInterval || $e8f16a459684b36f$export$31c453b5fe09788d;
171
+ this.onError = options.onError;
167
172
  if (this.debug) console.log("[AnalyticsReporter] Initiated AnalyticsReporter", options);
168
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
+ });
169
181
  $4cb1735bdc43624b$export$957313a2f485e5ed.prototype.init = function(sessionId) {
170
182
  return $4cb1735bdc43624b$var$__awaiter(this, void 0, Promise, function() {
171
- 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;
172
225
  return $4cb1735bdc43624b$var$__generator(this, function(_a) {
173
226
  switch(_a.label){
174
227
  case 0:
175
- this.sessionId = sessionId || this.sessionId;
176
- data = {
177
- event: "init",
178
- sessionId: this.sessionId,
179
- shardId: this.shardId,
180
- timestamp: Date.now(),
181
- playhead: -1,
182
- duration: -1
183
- };
184
- if (!this.debug) return [
185
- 3 /*break*/ ,
186
- 1
187
- ];
188
- console.log("[AnalyticsReporter] Init session:", data);
189
- this.isInitiated = true;
190
- return [
191
- 2 /*return*/ ,
192
- {
193
- sessionId: this.sessionId,
194
- heartbeatInterval: this.heartbeatInterval,
195
- isInitiated: this.isInitiated
196
- }
197
- ];
198
- case 1:
228
+ _a.trys.push([
229
+ 0,
230
+ 6,
231
+ ,
232
+ 7
233
+ ]);
199
234
  return [
200
235
  4 /*yield*/ ,
201
236
  fetch("".concat(this.eventsinkUrl), {
@@ -210,38 +245,150 @@ var $4cb1735bdc43624b$export$957313a2f485e5ed = function() {
210
245
  body: JSON.stringify(data)
211
246
  })
212
247
  ];
213
- case 2:
248
+ case 1:
214
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");
215
252
  if (!initResponse.ok) throw new Error("[AnalyticsReporter] init failed: ".concat(initResponse.statusText));
216
253
  return [
217
254
  4 /*yield*/ ,
218
255
  initResponse.json()
219
256
  ];
220
- case 3:
257
+ case 2:
221
258
  initResponseJson = _a.sent();
259
+ if (this.state === "destroyed") throw new Error("[AnalyticsReporter] init aborted: reporter was destroyed");
222
260
  if (!this.sessionId && !initResponseJson.sessionId) throw new Error("[AnalyticsReporter] init failed: no sessionId");
223
261
  if (initResponseJson.sessionId) this.sessionId = initResponseJson.sessionId;
224
- 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";
225
282
  return [
226
283
  2 /*return*/ ,
227
284
  {
228
285
  heartbeatInterval: this.heartbeatInterval,
229
286
  sessionId: this.sessionId,
230
- isInitiated: this.isInitiated
287
+ isInitiated: true
231
288
  }
232
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
+ ];
233
300
  }
234
301
  });
235
302
  });
236
303
  };
237
304
  $4cb1735bdc43624b$export$957313a2f485e5ed.prototype.send = function(data) {
238
- if (!this.isInitiated) return console.warn("[AnalyticsReporter] Cannot report before initiation:", data);
239
- var payload = $4cb1735bdc43624b$var$__assign({
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), {
240
329
  sessionId: this.sessionId,
241
330
  shardId: this.shardId
242
- }, data);
243
- if (this.debug) console.log("[AnalyticsReporter] Send payload:", payload);
244
- else fetch("".concat(this.eventsinkUrl), {
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), {
245
392
  method: "POST",
246
393
  mode: "cors",
247
394
  cache: "no-cache",
@@ -251,8 +398,73 @@ var $4cb1735bdc43624b$export$957313a2f485e5ed = function() {
251
398
  "X-EPAS-Version": $e8f16a459684b36f$export$8a65ed80196068d4
252
399
  },
253
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);
254
417
  });
255
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
+ };
256
468
  return $4cb1735bdc43624b$export$957313a2f485e5ed;
257
469
  }();
258
470
 
@@ -380,10 +592,11 @@ var $7d88e8cdd46a09a5$var$__generator = undefined && undefined.__generator || fu
380
592
  }), g;
381
593
  };
382
594
  var $7d88e8cdd46a09a5$export$f76c463fe9cb9f15 = function() {
383
- function $7d88e8cdd46a09a5$export$f76c463fe9cb9f15(eventsinkUrl, debug) {
595
+ function $7d88e8cdd46a09a5$export$f76c463fe9cb9f15(eventsinkUrl, debug, onError) {
384
596
  this.debug = false;
385
- this.debug = debug;
597
+ this.debug = debug !== null && debug !== void 0 ? debug : false;
386
598
  this.eventsinkUrl = eventsinkUrl;
599
+ this.onError = onError;
387
600
  }
388
601
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.initiateAnalyticsReporter = function(_a) {
389
602
  var sessionId = _a.sessionId, shardId = _a.shardId, _b = _a.heartbeatInterval, heartbeatInterval = _b === void 0 ? $e8f16a459684b36f$export$31c453b5fe09788d : _b;
@@ -397,7 +610,8 @@ var $7d88e8cdd46a09a5$export$f76c463fe9cb9f15 = function() {
397
610
  eventsinkUrl: this.eventsinkUrl,
398
611
  debug: this.debug,
399
612
  heartbeatInterval: heartbeatInterval,
400
- shardId: shardId
613
+ shardId: shardId,
614
+ onError: this.onError
401
615
  });
402
616
  return [
403
617
  4 /*yield*/ ,
@@ -417,52 +631,87 @@ var $7d88e8cdd46a09a5$export$f76c463fe9cb9f15 = function() {
417
631
  });
418
632
  });
419
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
+ };
420
641
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.init = function(data) {
642
+ if (!this.ensureReporter()) return;
421
643
  this.analyticsReporter.send(data);
422
644
  };
423
645
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.metadata = function(data) {
646
+ if (!this.ensureReporter()) return;
424
647
  this.analyticsReporter.send(data);
425
648
  };
426
649
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.heartbeat = function(data) {
650
+ if (!this.ensureReporter()) return;
427
651
  this.analyticsReporter.send(data);
428
652
  };
429
653
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.loading = function(data) {
654
+ if (!this.ensureReporter()) return;
430
655
  this.analyticsReporter.send(data);
431
656
  };
432
657
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.loaded = function(data) {
658
+ if (!this.ensureReporter()) return;
433
659
  this.analyticsReporter.send(data);
434
660
  };
435
661
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.playing = function(data) {
662
+ if (!this.ensureReporter()) return;
436
663
  this.analyticsReporter.send(data);
437
664
  };
438
665
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.pause = function(data) {
666
+ if (!this.ensureReporter()) return;
439
667
  this.analyticsReporter.send(data);
440
668
  };
441
669
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.buffering = function(data) {
670
+ if (!this.ensureReporter()) return;
442
671
  this.analyticsReporter.send(data);
443
672
  };
444
673
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.buffered = function(data) {
674
+ if (!this.ensureReporter()) return;
445
675
  this.analyticsReporter.send(data);
446
676
  };
447
677
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.seeking = function(data) {
678
+ if (!this.ensureReporter()) return;
448
679
  this.analyticsReporter.send(data);
449
680
  };
450
681
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.seeked = function(data) {
682
+ if (!this.ensureReporter()) return;
451
683
  this.analyticsReporter.send(data);
452
684
  };
453
685
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.bitrateChanged = function(data) {
686
+ if (!this.ensureReporter()) return;
454
687
  this.analyticsReporter.send(data);
455
688
  };
456
689
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.error = function(data) {
690
+ if (!this.ensureReporter()) return;
457
691
  this.analyticsReporter.send(data);
458
692
  };
459
693
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.warning = function(data) {
694
+ if (!this.ensureReporter()) return;
460
695
  this.analyticsReporter.send(data);
461
696
  };
462
697
  $7d88e8cdd46a09a5$export$f76c463fe9cb9f15.prototype.stopped = function(data) {
698
+ if (!this.ensureReporter()) return;
463
699
  this.analyticsReporter.send(data);
464
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
+ };
465
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();
466
715
  this.analyticsReporter = null;
467
716
  };
468
717
  return $7d88e8cdd46a09a5$export$f76c463fe9cb9f15;
@@ -607,33 +856,83 @@ var $c2e08ff125b5bcd1$var$__generator = undefined && undefined.__generator || fu
607
856
  }), g;
608
857
  };
609
858
  var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
610
- function $c2e08ff125b5bcd1$export$2daf9923c014a26e(eventsinkUrl, debug) {
859
+ function $c2e08ff125b5bcd1$export$2daf9923c014a26e(eventsinkUrl, debug, onError) {
860
+ var _this = this;
611
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
+ };
612
898
  this.eventsinkUrl = eventsinkUrl;
613
- this.playerAnalytics = new $7d88e8cdd46a09a5$exports.PlayerAnalytics(this.eventsinkUrl, debug);
899
+ this.playerAnalytics = new $7d88e8cdd46a09a5$exports.PlayerAnalytics(this.eventsinkUrl, debug, onError);
614
900
  }
615
901
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.init = function(options) {
616
902
  return $c2e08ff125b5bcd1$var$__awaiter(this, void 0, void 0, function() {
617
- var _a, heartbeatInterval, isInitiated;
618
- return $c2e08ff125b5bcd1$var$__generator(this, function(_b) {
619
- switch(_b.label){
620
- case 0:
621
- this.sessionId = options.sessionId;
622
- return [
623
- 4 /*yield*/ ,
624
- this.playerAnalytics.initiateAnalyticsReporter($c2e08ff125b5bcd1$var$__assign({
625
- sessionId: this.sessionId,
626
- shardId: options.shardId
627
- }, options))
628
- ];
629
- case 1:
630
- _a = _b.sent(), heartbeatInterval = _a.heartbeatInterval, isInitiated = _a.isInitiated;
631
- this.analyticsInitiated = isInitiated;
632
- this.heartbeatInterval = heartbeatInterval;
633
- return [
634
- 2 /*return*/
635
- ];
636
- }
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
+ ];
637
936
  });
638
937
  });
639
938
  };
@@ -643,6 +942,30 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
643
942
  event: "loading"
644
943
  }, this.playbackState()));
645
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);
646
969
  };
647
970
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.initiateVideoEventFilter = function() {
648
971
  var _this = this;
@@ -651,6 +974,11 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
651
974
  mediaElement: this.player,
652
975
  mp4Mode: false,
653
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();
654
982
  var eventType;
655
983
  var extraData = {
656
984
  };
@@ -678,32 +1006,65 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
678
1006
  eventType = "buffered";
679
1007
  break;
680
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;
681
1012
  eventType = "stopped";
682
1013
  extraData["reason"] = "ended";
1014
+ _this.stoppedReported = true;
683
1015
  _this.stopInterval();
684
1016
  break;
685
1017
  default:
686
1018
  break;
687
1019
  }
688
- try {
689
- if (!_this.analyticsInitiated) {
690
- console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
691
- return;
692
- }
693
- if (eventType) _this.playerAnalytics[eventType == "paused" ? "pause" : eventType]($c2e08ff125b5bcd1$var$__assign($c2e08ff125b5bcd1$var$__assign({
694
- event: eventType
695
- }, _this.playbackState()), Object.keys(extraData).length > 0 && {
696
- payload: extraData
697
- }));
698
- } catch (err) {
699
- console.error(err);
700
- }
1020
+ _this.sendFilteredEvent(eventType, extraData);
701
1021
  }
702
1022
  });
703
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
+ };
704
1061
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.startInterval = function() {
705
1062
  var _this = this;
706
1063
  if (this.heartbeatIntervalTimer) return;
1064
+ if (!this.heartbeatInterval) {
1065
+ this.pendingHeartbeatStart = true;
1066
+ return;
1067
+ }
707
1068
  this.heartbeatIntervalTimer = setInterval(function() {
708
1069
  _this.playerAnalytics.heartbeat($c2e08ff125b5bcd1$var$__assign({
709
1070
  event: "heartbeat"
@@ -712,9 +1073,11 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
712
1073
  };
713
1074
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.stopInterval = function() {
714
1075
  clearInterval(this.heartbeatIntervalTimer);
1076
+ this.heartbeatIntervalTimer = null;
1077
+ this.pendingHeartbeatStart = false;
715
1078
  };
716
1079
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.reportBitrateChange = function(payload) {
717
- if (!this.analyticsInitiated) {
1080
+ if (!this.initCalled) {
718
1081
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
719
1082
  return;
720
1083
  }
@@ -725,21 +1088,32 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
725
1088
  }));
726
1089
  };
727
1090
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.reportStop = function() {
728
- if (!this.analyticsInitiated) {
1091
+ if (!this.initCalled) {
729
1092
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
730
1093
  return;
731
1094
  }
732
- 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({
733
1108
  event: "stopped"
734
1109
  }, this.playbackState()), {
735
1110
  payload: {
736
1111
  reason: "aborted"
737
1112
  }
738
1113
  }));
739
- this.stopInterval();
740
1114
  };
741
1115
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.reportError = function(error) {
742
- if (!this.analyticsInitiated) {
1116
+ if (!this.initCalled) {
743
1117
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
744
1118
  return;
745
1119
  }
@@ -755,10 +1129,11 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
755
1129
  reason: "error"
756
1130
  }
757
1131
  }));
1132
+ this.stoppedReported = true;
758
1133
  this.stopInterval();
759
1134
  };
760
1135
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.reportMetadata = function(payload) {
761
- if (!this.analyticsInitiated) {
1136
+ if (!this.initCalled) {
762
1137
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
763
1138
  return;
764
1139
  }
@@ -769,7 +1144,7 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
769
1144
  }));
770
1145
  };
771
1146
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.reportWarning = function(payload) {
772
- if (!this.analyticsInitiated) {
1147
+ if (!this.initCalled) {
773
1148
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
774
1149
  return;
775
1150
  }
@@ -782,7 +1157,7 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
782
1157
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.playbackState = function() {
783
1158
  var _a, _b, _c, _d, _e;
784
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;
785
- 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;
786
1161
  return {
787
1162
  sessionId: this.sessionId,
788
1163
  timestamp: Date.now(),
@@ -790,25 +1165,73 @@ var $c2e08ff125b5bcd1$export$2daf9923c014a26e = function() {
790
1165
  duration: duration
791
1166
  };
792
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
+ };
793
1201
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.deinit = function() {
794
- if (!this.analyticsInitiated) {
1202
+ if (!this.initCalled) {
795
1203
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
796
1204
  return;
797
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();
798
1211
  this.stopInterval();
1212
+ this.removeUnloadListeners();
1213
+ this.detachAutoplayTerminalFallback();
799
1214
  this.heartbeatInterval = null;
800
1215
  this.videoEventFilter && this.videoEventFilter.teardown();
801
1216
  this.videoEventFilter = null;
1217
+ this.analyticsInitiated = false;
1218
+ this.initCalled = false;
802
1219
  };
803
1220
  $c2e08ff125b5bcd1$export$2daf9923c014a26e.prototype.destroy = function() {
804
- if (!this.analyticsInitiated) {
1221
+ if (!this.initCalled) {
805
1222
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
806
1223
  return;
807
1224
  }
1225
+ this.initGeneration++; // Invalidate any pending init callbacks
1226
+ this.stopInterval();
808
1227
  this.playerAnalytics.destroy();
1228
+ this.removeUnloadListeners();
1229
+ this.detachAutoplayTerminalFallback();
809
1230
  this.heartbeatInterval = null;
810
1231
  this.videoEventFilter && this.videoEventFilter.teardown();
811
- this.stopInterval();
1232
+ this.videoEventFilter = null;
1233
+ this.analyticsInitiated = false;
1234
+ this.initCalled = false;
812
1235
  };
813
1236
  return $c2e08ff125b5bcd1$export$2daf9923c014a26e;
814
1237
  }();