@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/module.js CHANGED
@@ -148,44 +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;
159
+ this.shardId = options.shardId;
155
160
  this.eventsinkUrl = options.eventsinkUrl;
156
161
  this.sessionId = options.sessionId;
157
162
  this.heartbeatInterval = options.heartbeatInterval || $76abaee2435acae3$export$31c453b5fe09788d;
163
+ this.onError = options.onError;
158
164
  if (this.debug) console.log("[AnalyticsReporter] Initiated AnalyticsReporter", options);
159
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
+ });
160
173
  $1d47cc84225ea891$export$957313a2f485e5ed.prototype.init = function(sessionId) {
161
174
  return $1d47cc84225ea891$var$__awaiter(this, void 0, Promise, function() {
162
- 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;
163
217
  return $1d47cc84225ea891$var$__generator(this, function(_a) {
164
218
  switch(_a.label){
165
219
  case 0:
166
- this.sessionId = sessionId || this.sessionId;
167
- data = {
168
- event: "init",
169
- sessionId: this.sessionId,
170
- timestamp: Date.now(),
171
- playhead: -1,
172
- duration: -1
173
- };
174
- if (!this.debug) return [
175
- 3 /*break*/ ,
176
- 1
177
- ];
178
- console.log("[AnalyticsReporter] Init session:", data);
179
- this.isInitiated = true;
180
- return [
181
- 2 /*return*/ ,
182
- {
183
- sessionId: this.sessionId,
184
- heartbeatInterval: this.heartbeatInterval,
185
- isInitiated: this.isInitiated
186
- }
187
- ];
188
- case 1:
220
+ _a.trys.push([
221
+ 0,
222
+ 6,
223
+ ,
224
+ 7
225
+ ]);
189
226
  return [
190
227
  4 /*yield*/ ,
191
228
  fetch("".concat(this.eventsinkUrl), {
@@ -200,37 +237,150 @@ var $1d47cc84225ea891$export$957313a2f485e5ed = function() {
200
237
  body: JSON.stringify(data)
201
238
  })
202
239
  ];
203
- case 2:
240
+ case 1:
204
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");
205
244
  if (!initResponse.ok) throw new Error("[AnalyticsReporter] init failed: ".concat(initResponse.statusText));
206
245
  return [
207
246
  4 /*yield*/ ,
208
247
  initResponse.json()
209
248
  ];
210
- case 3:
249
+ case 2:
211
250
  initResponseJson = _a.sent();
251
+ if (this.state === "destroyed") throw new Error("[AnalyticsReporter] init aborted: reporter was destroyed");
212
252
  if (!this.sessionId && !initResponseJson.sessionId) throw new Error("[AnalyticsReporter] init failed: no sessionId");
213
253
  if (initResponseJson.sessionId) this.sessionId = initResponseJson.sessionId;
214
- 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";
215
274
  return [
216
275
  2 /*return*/ ,
217
276
  {
218
277
  heartbeatInterval: this.heartbeatInterval,
219
278
  sessionId: this.sessionId,
220
- isInitiated: this.isInitiated
279
+ isInitiated: true
221
280
  }
222
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
+ ];
223
292
  }
224
293
  });
225
294
  });
226
295
  };
227
296
  $1d47cc84225ea891$export$957313a2f485e5ed.prototype.send = function(data) {
228
- if (!this.isInitiated) return console.warn("[AnalyticsReporter] Cannot report before initiation:", data);
229
- var payload = $1d47cc84225ea891$var$__assign({
230
- sessionId: this.sessionId
231
- }, data);
232
- if (this.debug) console.log("[AnalyticsReporter] Send payload:", payload);
233
- else fetch("".concat(this.eventsinkUrl), {
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), {
321
+ sessionId: this.sessionId,
322
+ shardId: this.shardId
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), {
234
384
  method: "POST",
235
385
  mode: "cors",
236
386
  cache: "no-cache",
@@ -240,8 +390,73 @@ var $1d47cc84225ea891$export$957313a2f485e5ed = function() {
240
390
  "X-EPAS-Version": $76abaee2435acae3$export$8a65ed80196068d4
241
391
  },
242
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);
243
409
  });
244
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
+ };
245
460
  return $1d47cc84225ea891$export$957313a2f485e5ed;
246
461
  }();
247
462
 
@@ -369,13 +584,14 @@ var $03bb5e0d0325c534$var$__generator = undefined && undefined.__generator || fu
369
584
  }), g;
370
585
  };
371
586
  var $03bb5e0d0325c534$export$f76c463fe9cb9f15 = function() {
372
- function $03bb5e0d0325c534$export$f76c463fe9cb9f15(eventsinkUrl, debug) {
587
+ function $03bb5e0d0325c534$export$f76c463fe9cb9f15(eventsinkUrl, debug, onError) {
373
588
  this.debug = false;
374
- this.debug = debug;
589
+ this.debug = debug !== null && debug !== void 0 ? debug : false;
375
590
  this.eventsinkUrl = eventsinkUrl;
591
+ this.onError = onError;
376
592
  }
377
593
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.initiateAnalyticsReporter = function(_a) {
378
- var sessionId = _a.sessionId, _b = _a.heartbeatInterval, heartbeatInterval = _b === void 0 ? $76abaee2435acae3$export$31c453b5fe09788d : _b;
594
+ var sessionId = _a.sessionId, shardId = _a.shardId, _b = _a.heartbeatInterval, heartbeatInterval = _b === void 0 ? $76abaee2435acae3$export$31c453b5fe09788d : _b;
379
595
  return $03bb5e0d0325c534$var$__awaiter(this, void 0, void 0, function() {
380
596
  var _c, generatedSessionId, isInitiated;
381
597
  return $03bb5e0d0325c534$var$__generator(this, function(_d) {
@@ -385,7 +601,9 @@ var $03bb5e0d0325c534$export$f76c463fe9cb9f15 = function() {
385
601
  sessionId: sessionId,
386
602
  eventsinkUrl: this.eventsinkUrl,
387
603
  debug: this.debug,
388
- heartbeatInterval: heartbeatInterval
604
+ heartbeatInterval: heartbeatInterval,
605
+ shardId: shardId,
606
+ onError: this.onError
389
607
  });
390
608
  return [
391
609
  4 /*yield*/ ,
@@ -405,52 +623,87 @@ var $03bb5e0d0325c534$export$f76c463fe9cb9f15 = function() {
405
623
  });
406
624
  });
407
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
+ };
408
633
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.init = function(data) {
634
+ if (!this.ensureReporter()) return;
409
635
  this.analyticsReporter.send(data);
410
636
  };
411
637
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.metadata = function(data) {
638
+ if (!this.ensureReporter()) return;
412
639
  this.analyticsReporter.send(data);
413
640
  };
414
641
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.heartbeat = function(data) {
642
+ if (!this.ensureReporter()) return;
415
643
  this.analyticsReporter.send(data);
416
644
  };
417
645
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.loading = function(data) {
646
+ if (!this.ensureReporter()) return;
418
647
  this.analyticsReporter.send(data);
419
648
  };
420
649
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.loaded = function(data) {
650
+ if (!this.ensureReporter()) return;
421
651
  this.analyticsReporter.send(data);
422
652
  };
423
653
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.playing = function(data) {
654
+ if (!this.ensureReporter()) return;
424
655
  this.analyticsReporter.send(data);
425
656
  };
426
657
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.pause = function(data) {
658
+ if (!this.ensureReporter()) return;
427
659
  this.analyticsReporter.send(data);
428
660
  };
429
661
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.buffering = function(data) {
662
+ if (!this.ensureReporter()) return;
430
663
  this.analyticsReporter.send(data);
431
664
  };
432
665
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.buffered = function(data) {
666
+ if (!this.ensureReporter()) return;
433
667
  this.analyticsReporter.send(data);
434
668
  };
435
669
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.seeking = function(data) {
670
+ if (!this.ensureReporter()) return;
436
671
  this.analyticsReporter.send(data);
437
672
  };
438
673
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.seeked = function(data) {
674
+ if (!this.ensureReporter()) return;
439
675
  this.analyticsReporter.send(data);
440
676
  };
441
677
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.bitrateChanged = function(data) {
678
+ if (!this.ensureReporter()) return;
442
679
  this.analyticsReporter.send(data);
443
680
  };
444
681
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.error = function(data) {
682
+ if (!this.ensureReporter()) return;
445
683
  this.analyticsReporter.send(data);
446
684
  };
447
685
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.warning = function(data) {
686
+ if (!this.ensureReporter()) return;
448
687
  this.analyticsReporter.send(data);
449
688
  };
450
689
  $03bb5e0d0325c534$export$f76c463fe9cb9f15.prototype.stopped = function(data) {
690
+ if (!this.ensureReporter()) return;
451
691
  this.analyticsReporter.send(data);
452
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
+ };
453
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();
454
707
  this.analyticsReporter = null;
455
708
  };
456
709
  return $03bb5e0d0325c534$export$f76c463fe9cb9f15;
@@ -595,32 +848,83 @@ var $85819f9bdebfd936$var$__generator = undefined && undefined.__generator || fu
595
848
  }), g;
596
849
  };
597
850
  var $85819f9bdebfd936$export$2daf9923c014a26e = function() {
598
- function $85819f9bdebfd936$export$2daf9923c014a26e(eventsinkUrl, debug) {
851
+ function $85819f9bdebfd936$export$2daf9923c014a26e(eventsinkUrl, debug, onError) {
852
+ var _this = this;
599
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
+ };
600
890
  this.eventsinkUrl = eventsinkUrl;
601
- this.playerAnalytics = new $03bb5e0d0325c534$exports.PlayerAnalytics(this.eventsinkUrl, debug);
891
+ this.playerAnalytics = new $03bb5e0d0325c534$exports.PlayerAnalytics(this.eventsinkUrl, debug, onError);
602
892
  }
603
893
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.init = function(options) {
604
894
  return $85819f9bdebfd936$var$__awaiter(this, void 0, void 0, function() {
605
- var _a, heartbeatInterval, isInitiated;
606
- return $85819f9bdebfd936$var$__generator(this, function(_b) {
607
- switch(_b.label){
608
- case 0:
609
- this.sessionId = options.sessionId;
610
- return [
611
- 4 /*yield*/ ,
612
- this.playerAnalytics.initiateAnalyticsReporter($85819f9bdebfd936$var$__assign({
613
- sessionId: this.sessionId
614
- }, options))
615
- ];
616
- case 1:
617
- _a = _b.sent(), heartbeatInterval = _a.heartbeatInterval, isInitiated = _a.isInitiated;
618
- this.analyticsInitiated = isInitiated;
619
- this.heartbeatInterval = heartbeatInterval;
620
- return [
621
- 2 /*return*/
622
- ];
623
- }
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
+ ];
624
928
  });
625
929
  });
626
930
  };
@@ -630,6 +934,30 @@ var $85819f9bdebfd936$export$2daf9923c014a26e = function() {
630
934
  event: "loading"
631
935
  }, this.playbackState()));
632
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);
633
961
  };
634
962
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.initiateVideoEventFilter = function() {
635
963
  var _this = this;
@@ -638,6 +966,11 @@ var $85819f9bdebfd936$export$2daf9923c014a26e = function() {
638
966
  mediaElement: this.player,
639
967
  mp4Mode: false,
640
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();
641
974
  var eventType;
642
975
  var extraData = {
643
976
  };
@@ -665,32 +998,65 @@ var $85819f9bdebfd936$export$2daf9923c014a26e = function() {
665
998
  eventType = "buffered";
666
999
  break;
667
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;
668
1004
  eventType = "stopped";
669
1005
  extraData["reason"] = "ended";
1006
+ _this.stoppedReported = true;
670
1007
  _this.stopInterval();
671
1008
  break;
672
1009
  default:
673
1010
  break;
674
1011
  }
675
- try {
676
- if (!_this.analyticsInitiated) {
677
- console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
678
- return;
679
- }
680
- if (eventType) _this.playerAnalytics[eventType == "paused" ? "pause" : eventType]($85819f9bdebfd936$var$__assign($85819f9bdebfd936$var$__assign({
681
- event: eventType
682
- }, _this.playbackState()), Object.keys(extraData).length > 0 && {
683
- payload: extraData
684
- }));
685
- } catch (err) {
686
- console.error(err);
687
- }
1012
+ _this.sendFilteredEvent(eventType, extraData);
688
1013
  }
689
1014
  });
690
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
+ };
691
1053
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.startInterval = function() {
692
1054
  var _this = this;
693
1055
  if (this.heartbeatIntervalTimer) return;
1056
+ if (!this.heartbeatInterval) {
1057
+ this.pendingHeartbeatStart = true;
1058
+ return;
1059
+ }
694
1060
  this.heartbeatIntervalTimer = setInterval(function() {
695
1061
  _this.playerAnalytics.heartbeat($85819f9bdebfd936$var$__assign({
696
1062
  event: "heartbeat"
@@ -699,9 +1065,11 @@ var $85819f9bdebfd936$export$2daf9923c014a26e = function() {
699
1065
  };
700
1066
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.stopInterval = function() {
701
1067
  clearInterval(this.heartbeatIntervalTimer);
1068
+ this.heartbeatIntervalTimer = null;
1069
+ this.pendingHeartbeatStart = false;
702
1070
  };
703
1071
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.reportBitrateChange = function(payload) {
704
- if (!this.analyticsInitiated) {
1072
+ if (!this.initCalled) {
705
1073
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
706
1074
  return;
707
1075
  }
@@ -712,21 +1080,32 @@ var $85819f9bdebfd936$export$2daf9923c014a26e = function() {
712
1080
  }));
713
1081
  };
714
1082
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.reportStop = function() {
715
- if (!this.analyticsInitiated) {
1083
+ if (!this.initCalled) {
716
1084
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
717
1085
  return;
718
1086
  }
719
- 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({
720
1100
  event: "stopped"
721
1101
  }, this.playbackState()), {
722
1102
  payload: {
723
1103
  reason: "aborted"
724
1104
  }
725
1105
  }));
726
- this.stopInterval();
727
1106
  };
728
1107
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.reportError = function(error) {
729
- if (!this.analyticsInitiated) {
1108
+ if (!this.initCalled) {
730
1109
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
731
1110
  return;
732
1111
  }
@@ -742,10 +1121,11 @@ var $85819f9bdebfd936$export$2daf9923c014a26e = function() {
742
1121
  reason: "error"
743
1122
  }
744
1123
  }));
1124
+ this.stoppedReported = true;
745
1125
  this.stopInterval();
746
1126
  };
747
1127
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.reportMetadata = function(payload) {
748
- if (!this.analyticsInitiated) {
1128
+ if (!this.initCalled) {
749
1129
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
750
1130
  return;
751
1131
  }
@@ -756,7 +1136,7 @@ var $85819f9bdebfd936$export$2daf9923c014a26e = function() {
756
1136
  }));
757
1137
  };
758
1138
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.reportWarning = function(payload) {
759
- if (!this.analyticsInitiated) {
1139
+ if (!this.initCalled) {
760
1140
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
761
1141
  return;
762
1142
  }
@@ -769,7 +1149,7 @@ var $85819f9bdebfd936$export$2daf9923c014a26e = function() {
769
1149
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.playbackState = function() {
770
1150
  var _a, _b, _c, _d, _e;
771
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;
772
- 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;
773
1153
  return {
774
1154
  sessionId: this.sessionId,
775
1155
  timestamp: Date.now(),
@@ -777,25 +1157,73 @@ var $85819f9bdebfd936$export$2daf9923c014a26e = function() {
777
1157
  duration: duration
778
1158
  };
779
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
+ };
780
1193
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.deinit = function() {
781
- if (!this.analyticsInitiated) {
1194
+ if (!this.initCalled) {
782
1195
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
783
1196
  return;
784
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();
785
1203
  this.stopInterval();
1204
+ this.removeUnloadListeners();
1205
+ this.detachAutoplayTerminalFallback();
786
1206
  this.heartbeatInterval = null;
787
1207
  this.videoEventFilter && this.videoEventFilter.teardown();
788
1208
  this.videoEventFilter = null;
1209
+ this.analyticsInitiated = false;
1210
+ this.initCalled = false;
789
1211
  };
790
1212
  $85819f9bdebfd936$export$2daf9923c014a26e.prototype.destroy = function() {
791
- if (!this.analyticsInitiated) {
1213
+ if (!this.initCalled) {
792
1214
  console.warn("[PlayerAnalyticsConnector] Analytics not initiated");
793
1215
  return;
794
1216
  }
1217
+ this.initGeneration++; // Invalidate any pending init callbacks
1218
+ this.stopInterval();
795
1219
  this.playerAnalytics.destroy();
1220
+ this.removeUnloadListeners();
1221
+ this.detachAutoplayTerminalFallback();
796
1222
  this.heartbeatInterval = null;
797
1223
  this.videoEventFilter && this.videoEventFilter.teardown();
798
- this.stopInterval();
1224
+ this.videoEventFilter = null;
1225
+ this.analyticsInitiated = false;
1226
+ this.initCalled = false;
799
1227
  };
800
1228
  return $85819f9bdebfd936$export$2daf9923c014a26e;
801
1229
  }();