@guardvideo/player-sdk 3.3.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;YA4Cf,cAAc;YAsDd,YAAY;YAqCZ,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,27 +663,34 @@ 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;
620
681
  }
621
682
  const challenge = await resp.json();
622
- this.log('Challenge received, computing PBKDF2 response...');
683
+ this.log('Challenge received, computing PBKDF2 proof-of-work...');
623
684
  const encoder = new TextEncoder();
624
685
  const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(challenge.nonce), 'PBKDF2', false, ['deriveBits']);
625
- const derivedBits = await crypto.subtle.deriveBits({
686
+ await crypto.subtle.deriveBits({
626
687
  name: 'PBKDF2',
627
688
  salt: encoder.encode(challenge.salt),
628
689
  iterations: challenge.iterations,
629
690
  hash: 'SHA-256',
630
691
  }, keyMaterial, challenge.keyLength * 8);
631
- const hashArray = Array.from(new Uint8Array(derivedBits));
632
- const hexResponse = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
633
- this.log('Challenge response computed');
634
- return hexResponse;
692
+ this.log('Challenge proof-of-work completed');
693
+ return { nonce: challenge.nonce };
635
694
  }
636
695
  catch (err) {
637
696
  this.log('Challenge computation failed (non-fatal):', err);
@@ -641,12 +700,16 @@ class GuardVideoPlayer {
641
700
  async acquireNonce(tokenId) {
642
701
  if (!this.config.apiBaseUrl)
643
702
  return null;
703
+ if (Date.now() < this.rateLimitCooldownUntil) {
704
+ this.log('Nonce acquisition skipped — rate limit cooldown active');
705
+ return this.playlistNonce;
706
+ }
644
707
  try {
645
- const challengeResponse = await this.solveChallenge(tokenId);
708
+ const challengeResult = await this.solveChallenge(tokenId);
646
709
  const url = this.config.apiBaseUrl + '/videos/playlist-session';
647
710
  const body = { tokenId };
648
- if (challengeResponse) {
649
- body.challengeResponse = challengeResponse;
711
+ if (challengeResult) {
712
+ body.challengeNonce = challengeResult.nonce;
650
713
  }
651
714
  const resp = await fetch(url, {
652
715
  method: 'POST',
@@ -654,12 +717,19 @@ class GuardVideoPlayer {
654
717
  body: JSON.stringify(body),
655
718
  credentials: 'omit',
656
719
  });
720
+ if (resp.status === 429) {
721
+ this.enterRateLimitCooldown();
722
+ this.log('Nonce acquisition hit 429 — entering cooldown');
723
+ return this.playlistNonce;
724
+ }
657
725
  if (!resp.ok) {
658
726
  this.log('Playlist session nonce acquisition failed', resp.status);
659
727
  return null;
660
728
  }
661
729
  const data = await resp.json();
662
730
  this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
731
+ this.retryBackoff = 1000;
732
+ this.scheduleNonceRefresh(data.expiresIn);
663
733
  return data.nonce;
664
734
  }
665
735
  catch (err) {
@@ -667,19 +737,38 @@ class GuardVideoPlayer {
667
737
  return null;
668
738
  }
669
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
+ }
670
750
  async refreshNonce() {
671
- if (this.nonceRefreshInProgress)
672
- return;
751
+ if (this.nonceRefreshInProgress && this.nonceRefreshPromise) {
752
+ return this.nonceRefreshPromise;
753
+ }
673
754
  if (!this.embedToken)
674
755
  return;
675
756
  this.nonceRefreshInProgress = true;
676
- try {
677
- const nonce = await this.acquireNonce(this.embedToken.tokenId);
678
- this.playlistNonce = nonce;
679
- }
680
- finally {
681
- this.nonceRefreshInProgress = false;
682
- }
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);
683
772
  }
684
773
  async initializePlayer() {
685
774
  if (!this.embedToken) {
@@ -719,8 +808,12 @@ class GuardVideoPlayer {
719
808
  enableWorker: true,
720
809
  lowLatencyMode: false,
721
810
  liveSyncDurationCount: 3,
722
- manifestLoadingMaxRetry: 6,
723
- levelLoadingMaxRetry: 6,
811
+ manifestLoadingMaxRetry: 3,
812
+ manifestLoadingRetryDelay: 2000,
813
+ levelLoadingMaxRetry: 3,
814
+ levelLoadingRetryDelay: 2000,
815
+ fragLoadingMaxRetry: 4,
816
+ fragLoadingRetryDelay: 1000,
724
817
  xhrSetup(xhr, url) {
725
818
  if (url.includes('/watermark-stream/') && url.includes('.m3u8')) {
726
819
  if (self.playlistNonce) {
@@ -728,10 +821,6 @@ class GuardVideoPlayer {
728
821
  const nonceUrl = url + separator + 'nonce=' + encodeURIComponent(self.playlistNonce);
729
822
  xhr.open('GET', nonceUrl, true);
730
823
  self.log('Injected nonce into playlist request');
731
- self.playlistNonce = null;
732
- self.refreshNonce().catch(() => {
733
- self.log('Background nonce refresh failed');
734
- });
735
824
  }
736
825
  }
737
826
  },
@@ -743,6 +832,7 @@ class GuardVideoPlayer {
743
832
  this.log('HLS manifest parsed', { levels: data.levels.map((l) => l.height + 'p') });
744
833
  this.setState(PlayerState.READY);
745
834
  this.config.onReady();
835
+ this.retryBackoff = 1000;
746
836
  if (this.config.autoplay)
747
837
  this.play();
748
838
  });
@@ -758,18 +848,52 @@ class GuardVideoPlayer {
758
848
  this.currentQuality = quality;
759
849
  this.log('Quality switched to ' + quality.name);
760
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
+ });
761
856
  });
762
857
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
763
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
+ }
764
887
  if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
765
888
  data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
766
889
  this.embedToken?.forensicWatermark) {
767
890
  this.log('Playlist load failed — refreshing nonce before retry');
768
891
  this.refreshNonce().then(() => {
769
- setTimeout(() => this.hls?.startLoad(), 500);
892
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
770
893
  }).catch(() => {
771
- setTimeout(() => this.hls?.startLoad(), 1000);
894
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
772
895
  });
896
+ this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
773
897
  return;
774
898
  }
775
899
  if (data.fatal) {
@@ -779,7 +903,8 @@ class GuardVideoPlayer {
779
903
  if (this.embedToken?.forensicWatermark) {
780
904
  this.refreshNonce().catch(() => { });
781
905
  }
782
- setTimeout(() => this.hls?.startLoad(), 1000);
906
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
907
+ this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
783
908
  break;
784
909
  case Hls.ErrorTypes.MEDIA_ERROR:
785
910
  this.error('Media error, attempting recovery...');
@@ -857,6 +982,10 @@ class GuardVideoPlayer {
857
982
  getState() { return this.state; }
858
983
  destroy() {
859
984
  this.log('Destroying player');
985
+ if (this.nonceRefreshTimer) {
986
+ clearTimeout(this.nonceRefreshTimer);
987
+ this.nonceRefreshTimer = null;
988
+ }
860
989
  if (this.eventTracker) {
861
990
  this.eventTracker.destroy();
862
991
  this.eventTracker = null;