@guardvideo/player-sdk 3.4.0 → 3.5.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.
@@ -21,6 +21,8 @@ export declare class EventTracker {
21
21
  private accumulator;
22
22
  private destroyed;
23
23
  private _onBeforeUnload;
24
+ private flushBackoff;
25
+ private flushing;
24
26
  constructor(config: EventTrackerConfig);
25
27
  track(type: string, positionSeconds?: number, payload?: Record<string, unknown>): void;
26
28
  startChunk(position: number): void;
@@ -30,6 +32,7 @@ export declare class EventTracker {
30
32
  flush(): Promise<void>;
31
33
  destroy(): void;
32
34
  private startAutoFlush;
35
+ private restartAutoFlush;
33
36
  private hookPageUnload;
34
37
  private sendBatch;
35
38
  private randomNonce;
@@ -1 +1 @@
1
- {"version":3,"file":"EventTracker.d.ts","sourceRoot":"","sources":["../../src/core/EventTracker.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAEjC,QAAQ,EAAE,MAAM,CAAC;IAEjB,OAAO,EAAE,MAAM,CAAC;IAEhB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,YAAY,EAAE,MAAM,CAAC;IAErB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAKD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,eAAe,CAA6B;gBAExC,MAAM,EAAE,kBAAkB;IAkBtC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IActF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKlC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKjC,QAAQ,IAAI,IAAI;IAKhB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAK/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB5B,OAAO,IAAI,IAAI;IAYf,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,cAAc;YA+BR,SAAS;IAsCvB,OAAO,CAAC,WAAW;YASL,IAAI;CAenB"}
1
+ {"version":3,"file":"EventTracker.d.ts","sourceRoot":"","sources":["../../src/core/EventTracker.ts"],"names":[],"mappings":"AAiCA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAEjC,QAAQ,EAAE,MAAM,CAAC;IAEjB,OAAO,EAAE,MAAM,CAAC;IAEhB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,YAAY,EAAE,MAAM,CAAC;IAErB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,eAAe,CAA6B;IAEpD,OAAO,CAAC,YAAY,CAAK;IAEzB,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,kBAAkB;IAoBtC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAqBtF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKlC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKjC,QAAQ,IAAI,IAAI;IAKhB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAK/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB5B,OAAO,IAAI,IAAI;IAYf,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,cAAc;YA+BR,SAAS;IA4DvB,OAAO,CAAC,WAAW;YASL,IAAI;CAenB"}
@@ -13,6 +13,11 @@ export declare class GuardVideoPlayer implements PlayerInstance {
13
13
  private sessionId;
14
14
  private playlistNonce;
15
15
  private nonceRefreshInProgress;
16
+ private nonceRefreshTimer;
17
+ private retryBackoff;
18
+ private readonly MAX_BACKOFF;
19
+ private rateLimitCooldownUntil;
20
+ private nonceRefreshPromise;
16
21
  private _onRateChange;
17
22
  constructor(videoElement: HTMLVideoElement, videoId: string, config: PlayerConfig);
18
23
  private log;
@@ -27,7 +32,9 @@ export declare class GuardVideoPlayer implements PlayerInstance {
27
32
  private fetchEmbedToken;
28
33
  private solveChallenge;
29
34
  private acquireNonce;
35
+ private scheduleNonceRefresh;
30
36
  private refreshNonce;
37
+ private enterRateLimitCooldown;
31
38
  private initializePlayer;
32
39
  private initializeHls;
33
40
  private setupVideoEventListeners;
@@ -1 +1 @@
1
- {"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../../src/core/player.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,YAAY,EAEZ,WAAW,EACX,cAAc,EACd,YAAY,EAIb,MAAM,SAAS,CAAC;AA8BjB,qBAAa,gBAAiB,YAAW,cAAc;IAqBnD,OAAO,CAAC,OAAO;IApBjB,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,UAAU,CAAmC;IACrD,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,SAAS,CAAqB;IAEtC,OAAO,CAAC,aAAa,CAAuB;IAE5C,OAAO,CAAC,sBAAsB,CAAS;IAGvC,OAAO,CAAC,aAAa,CAAkC;gBAGrD,YAAY,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,EACvB,MAAM,EAAE,YAAY;IAqDtB,OAAO,CAAC,GAAG;IAMX,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,kBAAkB;IA2B1B,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,cAAc;YAWR,UAAU;YAkCV,sBAAsB;IA6CpC,OAAO,CAAC,gBAAgB;YA0CV,eAAe;YAgDf,cAAc;YAqDd,YAAY;YAsCZ,YAAY;YAYZ,gBAAgB;IAiC9B,OAAO,CAAC,aAAa;IAoGrB,OAAO,CAAC,wBAAwB;IAuBhC,OAAO,CAAC,WAAW;IASN,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B,KAAK,IAAI,IAAI;IAEb,cAAc,IAAI,MAAM;IAExB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAExB,WAAW,IAAI,MAAM;IAErB,SAAS,IAAI,MAAM;IAEnB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,gBAAgB,IAAI,YAAY,EAAE;IAWlC,iBAAiB,IAAI,YAAY,GAAG,IAAI;IAExC,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOpC,QAAQ,IAAI,WAAW;IAEvB,OAAO,IAAI,IAAI;CA0BvB"}
1
+ {"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../../src/core/player.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,YAAY,EAEZ,WAAW,EACX,cAAc,EACd,YAAY,EAIb,MAAM,SAAS,CAAC;AA8BjB,qBAAa,gBAAiB,YAAW,cAAc;IA+BnD,OAAO,CAAC,OAAO;IA9BjB,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,UAAU,CAAmC;IACrD,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,SAAS,CAAqB;IAEtC,OAAO,CAAC,aAAa,CAAuB;IAE5C,OAAO,CAAC,sBAAsB,CAAS;IAEvC,OAAO,CAAC,iBAAiB,CAA8C;IAEvE,OAAO,CAAC,YAAY,CAAQ;IAE5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,OAAO,CAAC,sBAAsB,CAAK;IAEnC,OAAO,CAAC,mBAAmB,CAA8B;IAGzD,OAAO,CAAC,aAAa,CAAkC;gBAGrD,YAAY,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,EACvB,MAAM,EAAE,YAAY;IAqDtB,OAAO,CAAC,GAAG;IAMX,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,kBAAkB;IA2B1B,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,cAAc;YAWR,UAAU;YAkCV,sBAAsB;IA6CpC,OAAO,CAAC,gBAAgB;YA0CV,eAAe;YAgDf,cAAc;YAmEd,YAAY;IAqD1B,OAAO,CAAC,oBAAoB;YAed,YAAY;IAmB1B,OAAO,CAAC,sBAAsB;YAKhB,gBAAgB;IAiC9B,OAAO,CAAC,aAAa;IA6IrB,OAAO,CAAC,wBAAwB;IAuBhC,OAAO,CAAC,WAAW;IASN,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B,KAAK,IAAI,IAAI;IAEb,cAAc,IAAI,MAAM;IAExB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAExB,WAAW,IAAI,MAAM;IAErB,SAAS,IAAI,MAAM;IAEnB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,gBAAgB,IAAI,YAAY,EAAE;IAWlC,iBAAiB,IAAI,YAAY,GAAG,IAAI;IAExC,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOpC,QAAQ,IAAI,WAAW;IAEvB,OAAO,IAAI,IAAI;CAgCvB"}
package/dist/index.esm.js CHANGED
@@ -68,8 +68,19 @@ class WatchChunkAccumulator {
68
68
  }
69
69
  }
70
70
 
71
+ const VALID_EVENT_TYPES = [
72
+ 'watch_chunk',
73
+ 'play',
74
+ 'pause',
75
+ 'seek',
76
+ 'ended',
77
+ 'quality_change',
78
+ 'error',
79
+ 'security_event',
80
+ ];
71
81
  const MAX_BATCH_SIZE = 25;
72
82
  const DEFAULT_FLUSH_INTERVAL = 5000;
83
+ const MAX_FLUSH_BACKOFF = 60000;
73
84
  class EventTracker {
74
85
  constructor(config) {
75
86
  this.buffer = [];
@@ -77,6 +88,8 @@ class EventTracker {
77
88
  this.accumulator = new WatchChunkAccumulator();
78
89
  this.destroyed = false;
79
90
  this._onBeforeUnload = null;
91
+ this.flushBackoff = 0;
92
+ this.flushing = false;
80
93
  this.config = {
81
94
  endpoint: config.endpoint,
82
95
  tokenId: config.tokenId,
@@ -92,6 +105,12 @@ class EventTracker {
92
105
  track(type, positionSeconds, payload) {
93
106
  if (this.destroyed)
94
107
  return;
108
+ if (!VALID_EVENT_TYPES.includes(type)) {
109
+ if (this.config.debug) {
110
+ console.warn('[GuardVideo EventTracker] Skipping unknown event type:', type);
111
+ }
112
+ return;
113
+ }
95
114
  this.buffer.push({
96
115
  event_type: type,
97
116
  event_at: new Date().toISOString(),
@@ -115,7 +134,7 @@ class EventTracker {
115
134
  this.config.sessionId = sessionId;
116
135
  }
117
136
  async flush() {
118
- if (this.destroyed)
137
+ if (this.destroyed || this.flushing)
119
138
  return;
120
139
  const chunks = this.accumulator.drain();
121
140
  for (const c of chunks) {
@@ -123,8 +142,14 @@ class EventTracker {
123
142
  }
124
143
  if (this.buffer.length === 0)
125
144
  return;
126
- const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
127
- await this.sendBatch(batch);
145
+ this.flushing = true;
146
+ try {
147
+ const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
148
+ await this.sendBatch(batch);
149
+ }
150
+ finally {
151
+ this.flushing = false;
152
+ }
128
153
  }
129
154
  destroy() {
130
155
  this.destroyed = true;
@@ -139,6 +164,11 @@ class EventTracker {
139
164
  startAutoFlush() {
140
165
  this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
141
166
  }
167
+ restartAutoFlush(intervalMs) {
168
+ if (this.flushTimer)
169
+ clearInterval(this.flushTimer);
170
+ this.flushTimer = setInterval(() => this.flush(), intervalMs);
171
+ }
142
172
  hookPageUnload() {
143
173
  if (typeof window === 'undefined')
144
174
  return;
@@ -184,8 +214,25 @@ class EventTracker {
184
214
  body,
185
215
  credentials: 'omit',
186
216
  });
187
- if (!resp.ok && this.config.debug) {
188
- console.warn('[GuardVideo EventTracker] Flush failed:', resp.status);
217
+ if (!resp.ok) {
218
+ if (resp.status === 429) {
219
+ this.flushBackoff = Math.min((this.flushBackoff || this.config.flushIntervalMs) * 2, MAX_FLUSH_BACKOFF);
220
+ if (this.config.debug) {
221
+ console.warn('[GuardVideo EventTracker] 429 — backing off to', this.flushBackoff, 'ms');
222
+ }
223
+ this.buffer.unshift(...events);
224
+ this.restartAutoFlush(this.flushBackoff);
225
+ return;
226
+ }
227
+ if (this.config.debug) {
228
+ console.warn('[GuardVideo EventTracker] Flush failed:', resp.status);
229
+ }
230
+ }
231
+ else {
232
+ if (this.flushBackoff > 0) {
233
+ this.flushBackoff = 0;
234
+ this.restartAutoFlush(this.config.flushIntervalMs);
235
+ }
189
236
  }
190
237
  }
191
238
  catch (err) {
@@ -401,6 +448,11 @@ class GuardVideoPlayer {
401
448
  this._onSecurityEvent = null;
402
449
  this.playlistNonce = null;
403
450
  this.nonceRefreshInProgress = false;
451
+ this.nonceRefreshTimer = null;
452
+ this.retryBackoff = 1000;
453
+ this.MAX_BACKOFF = 30000;
454
+ this.rateLimitCooldownUntil = 0;
455
+ this.nonceRefreshPromise = null;
404
456
  this._onRateChange = this.enforceMaxRate.bind(this);
405
457
  this.videoElement = videoElement;
406
458
  this.config = {
@@ -611,9 +663,18 @@ class GuardVideoPlayer {
611
663
  async solveChallenge(tokenId) {
612
664
  if (!this.config.apiBaseUrl)
613
665
  return undefined;
666
+ if (Date.now() < this.rateLimitCooldownUntil) {
667
+ this.log('Challenge skipped — rate limit cooldown active');
668
+ return undefined;
669
+ }
614
670
  try {
615
671
  const url = this.config.apiBaseUrl + '/videos/challenge/' + encodeURIComponent(tokenId);
616
672
  const resp = await fetch(url, { credentials: 'omit' });
673
+ if (resp.status === 429) {
674
+ this.enterRateLimitCooldown();
675
+ this.log('Challenge hit 429 — entering cooldown');
676
+ return undefined;
677
+ }
617
678
  if (!resp.ok) {
618
679
  this.log('Challenge fetch failed (challenge may be disabled)', resp.status);
619
680
  return undefined;
@@ -639,6 +700,10 @@ class GuardVideoPlayer {
639
700
  async acquireNonce(tokenId) {
640
701
  if (!this.config.apiBaseUrl)
641
702
  return null;
703
+ if (Date.now() < this.rateLimitCooldownUntil) {
704
+ this.log('Nonce acquisition skipped — rate limit cooldown active');
705
+ return this.playlistNonce;
706
+ }
642
707
  try {
643
708
  const challengeResult = await this.solveChallenge(tokenId);
644
709
  const url = this.config.apiBaseUrl + '/videos/playlist-session';
@@ -652,12 +717,19 @@ class GuardVideoPlayer {
652
717
  body: JSON.stringify(body),
653
718
  credentials: 'omit',
654
719
  });
720
+ if (resp.status === 429) {
721
+ this.enterRateLimitCooldown();
722
+ this.log('Nonce acquisition hit 429 — entering cooldown');
723
+ return this.playlistNonce;
724
+ }
655
725
  if (!resp.ok) {
656
726
  this.log('Playlist session nonce acquisition failed', resp.status);
657
727
  return null;
658
728
  }
659
729
  const data = await resp.json();
660
730
  this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
731
+ this.retryBackoff = 1000;
732
+ this.scheduleNonceRefresh(data.expiresIn);
661
733
  return data.nonce;
662
734
  }
663
735
  catch (err) {
@@ -665,19 +737,38 @@ class GuardVideoPlayer {
665
737
  return null;
666
738
  }
667
739
  }
740
+ scheduleNonceRefresh(expiresInSeconds) {
741
+ if (this.nonceRefreshTimer)
742
+ clearTimeout(this.nonceRefreshTimer);
743
+ const refreshMs = Math.max(5000, expiresInSeconds * 750);
744
+ this.nonceRefreshTimer = setTimeout(() => {
745
+ this.refreshNonce().catch(() => {
746
+ this.log('Proactive nonce refresh failed');
747
+ });
748
+ }, refreshMs);
749
+ }
668
750
  async refreshNonce() {
669
- if (this.nonceRefreshInProgress)
670
- return;
751
+ if (this.nonceRefreshInProgress && this.nonceRefreshPromise) {
752
+ return this.nonceRefreshPromise;
753
+ }
671
754
  if (!this.embedToken)
672
755
  return;
673
756
  this.nonceRefreshInProgress = true;
674
- try {
675
- const nonce = await this.acquireNonce(this.embedToken.tokenId);
676
- this.playlistNonce = nonce;
677
- }
678
- finally {
679
- this.nonceRefreshInProgress = false;
680
- }
757
+ this.nonceRefreshPromise = (async () => {
758
+ try {
759
+ const nonce = await this.acquireNonce(this.embedToken.tokenId);
760
+ this.playlistNonce = nonce;
761
+ }
762
+ finally {
763
+ this.nonceRefreshInProgress = false;
764
+ this.nonceRefreshPromise = null;
765
+ }
766
+ })();
767
+ return this.nonceRefreshPromise;
768
+ }
769
+ enterRateLimitCooldown() {
770
+ this.rateLimitCooldownUntil = Date.now() + this.retryBackoff;
771
+ this.retryBackoff = Math.min(this.retryBackoff * 2, this.MAX_BACKOFF);
681
772
  }
682
773
  async initializePlayer() {
683
774
  if (!this.embedToken) {
@@ -717,8 +808,12 @@ class GuardVideoPlayer {
717
808
  enableWorker: true,
718
809
  lowLatencyMode: false,
719
810
  liveSyncDurationCount: 3,
720
- manifestLoadingMaxRetry: 6,
721
- levelLoadingMaxRetry: 6,
811
+ manifestLoadingMaxRetry: 3,
812
+ manifestLoadingRetryDelay: 2000,
813
+ levelLoadingMaxRetry: 3,
814
+ levelLoadingRetryDelay: 2000,
815
+ fragLoadingMaxRetry: 4,
816
+ fragLoadingRetryDelay: 1000,
722
817
  xhrSetup(xhr, url) {
723
818
  if (url.includes('/watermark-stream/') && url.includes('.m3u8')) {
724
819
  if (self.playlistNonce) {
@@ -726,10 +821,6 @@ class GuardVideoPlayer {
726
821
  const nonceUrl = url + separator + 'nonce=' + encodeURIComponent(self.playlistNonce);
727
822
  xhr.open('GET', nonceUrl, true);
728
823
  self.log('Injected nonce into playlist request');
729
- self.playlistNonce = null;
730
- self.refreshNonce().catch(() => {
731
- self.log('Background nonce refresh failed');
732
- });
733
824
  }
734
825
  }
735
826
  },
@@ -741,6 +832,7 @@ class GuardVideoPlayer {
741
832
  this.log('HLS manifest parsed', { levels: data.levels.map((l) => l.height + 'p') });
742
833
  this.setState(PlayerState.READY);
743
834
  this.config.onReady();
835
+ this.retryBackoff = 1000;
744
836
  if (this.config.autoplay)
745
837
  this.play();
746
838
  });
@@ -756,18 +848,52 @@ class GuardVideoPlayer {
756
848
  this.currentQuality = quality;
757
849
  this.log('Quality switched to ' + quality.name);
758
850
  this.config.onQualityChange(quality.name);
851
+ this.eventTracker?.track('quality_change', this.videoElement.currentTime, {
852
+ level: data.level,
853
+ height: level.height,
854
+ bitrate: level.bitrate,
855
+ });
759
856
  });
760
857
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
761
858
  this.error('HLS Error', data);
859
+ this.eventTracker?.track('error', this.videoElement.currentTime, {
860
+ type: data.type,
861
+ details: data.details,
862
+ fatal: data.fatal,
863
+ });
864
+ if (Date.now() < this.rateLimitCooldownUntil) {
865
+ this.log('Suppressing retry — rate limit cooldown active (' +
866
+ Math.ceil((this.rateLimitCooldownUntil - Date.now()) / 1000) + 's remaining)');
867
+ if (data.fatal) {
868
+ const delay = this.rateLimitCooldownUntil - Date.now() + 500;
869
+ setTimeout(() => this.hls?.startLoad(), delay);
870
+ }
871
+ return;
872
+ }
873
+ const httpStatus = data.response?.code;
874
+ if (httpStatus === 429) {
875
+ this.enterRateLimitCooldown();
876
+ this.log('429 detected — entering cooldown for ' + this.retryBackoff + 'ms');
877
+ setTimeout(() => {
878
+ if (this.embedToken?.forensicWatermark) {
879
+ this.refreshNonce().then(() => this.hls?.startLoad()).catch(() => this.hls?.startLoad());
880
+ }
881
+ else {
882
+ this.hls?.startLoad();
883
+ }
884
+ }, this.retryBackoff);
885
+ return;
886
+ }
762
887
  if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
763
888
  data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
764
889
  this.embedToken?.forensicWatermark) {
765
890
  this.log('Playlist load failed — refreshing nonce before retry');
766
891
  this.refreshNonce().then(() => {
767
- setTimeout(() => this.hls?.startLoad(), 500);
892
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
768
893
  }).catch(() => {
769
- setTimeout(() => this.hls?.startLoad(), 1000);
894
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
770
895
  });
896
+ this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
771
897
  return;
772
898
  }
773
899
  if (data.fatal) {
@@ -777,7 +903,8 @@ class GuardVideoPlayer {
777
903
  if (this.embedToken?.forensicWatermark) {
778
904
  this.refreshNonce().catch(() => { });
779
905
  }
780
- setTimeout(() => this.hls?.startLoad(), 1000);
906
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
907
+ this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
781
908
  break;
782
909
  case Hls.ErrorTypes.MEDIA_ERROR:
783
910
  this.error('Media error, attempting recovery...');
@@ -855,6 +982,10 @@ class GuardVideoPlayer {
855
982
  getState() { return this.state; }
856
983
  destroy() {
857
984
  this.log('Destroying player');
985
+ if (this.nonceRefreshTimer) {
986
+ clearTimeout(this.nonceRefreshTimer);
987
+ this.nonceRefreshTimer = null;
988
+ }
858
989
  if (this.eventTracker) {
859
990
  this.eventTracker.destroy();
860
991
  this.eventTracker = null;