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