@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.
package/dist/index.js CHANGED
@@ -70,8 +70,19 @@ class WatchChunkAccumulator {
70
70
  }
71
71
  }
72
72
 
73
+ const VALID_EVENT_TYPES = [
74
+ 'watch_chunk',
75
+ 'play',
76
+ 'pause',
77
+ 'seek',
78
+ 'ended',
79
+ 'quality_change',
80
+ 'error',
81
+ 'security_event',
82
+ ];
73
83
  const MAX_BATCH_SIZE = 25;
74
84
  const DEFAULT_FLUSH_INTERVAL = 5000;
85
+ const MAX_FLUSH_BACKOFF = 60000;
75
86
  class EventTracker {
76
87
  constructor(config) {
77
88
  this.buffer = [];
@@ -79,6 +90,8 @@ class EventTracker {
79
90
  this.accumulator = new WatchChunkAccumulator();
80
91
  this.destroyed = false;
81
92
  this._onBeforeUnload = null;
93
+ this.flushBackoff = 0;
94
+ this.flushing = false;
82
95
  this.config = {
83
96
  endpoint: config.endpoint,
84
97
  tokenId: config.tokenId,
@@ -94,6 +107,12 @@ class EventTracker {
94
107
  track(type, positionSeconds, payload) {
95
108
  if (this.destroyed)
96
109
  return;
110
+ if (!VALID_EVENT_TYPES.includes(type)) {
111
+ if (this.config.debug) {
112
+ console.warn('[GuardVideo EventTracker] Skipping unknown event type:', type);
113
+ }
114
+ return;
115
+ }
97
116
  this.buffer.push({
98
117
  event_type: type,
99
118
  event_at: new Date().toISOString(),
@@ -117,7 +136,7 @@ class EventTracker {
117
136
  this.config.sessionId = sessionId;
118
137
  }
119
138
  async flush() {
120
- if (this.destroyed)
139
+ if (this.destroyed || this.flushing)
121
140
  return;
122
141
  const chunks = this.accumulator.drain();
123
142
  for (const c of chunks) {
@@ -125,8 +144,14 @@ class EventTracker {
125
144
  }
126
145
  if (this.buffer.length === 0)
127
146
  return;
128
- const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
129
- await this.sendBatch(batch);
147
+ this.flushing = true;
148
+ try {
149
+ const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
150
+ await this.sendBatch(batch);
151
+ }
152
+ finally {
153
+ this.flushing = false;
154
+ }
130
155
  }
131
156
  destroy() {
132
157
  this.destroyed = true;
@@ -141,6 +166,11 @@ class EventTracker {
141
166
  startAutoFlush() {
142
167
  this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
143
168
  }
169
+ restartAutoFlush(intervalMs) {
170
+ if (this.flushTimer)
171
+ clearInterval(this.flushTimer);
172
+ this.flushTimer = setInterval(() => this.flush(), intervalMs);
173
+ }
144
174
  hookPageUnload() {
145
175
  if (typeof window === 'undefined')
146
176
  return;
@@ -186,8 +216,25 @@ class EventTracker {
186
216
  body,
187
217
  credentials: 'omit',
188
218
  });
189
- if (!resp.ok && this.config.debug) {
190
- console.warn('[GuardVideo EventTracker] Flush failed:', resp.status);
219
+ if (!resp.ok) {
220
+ if (resp.status === 429) {
221
+ this.flushBackoff = Math.min((this.flushBackoff || this.config.flushIntervalMs) * 2, MAX_FLUSH_BACKOFF);
222
+ if (this.config.debug) {
223
+ console.warn('[GuardVideo EventTracker] 429 — backing off to', this.flushBackoff, 'ms');
224
+ }
225
+ this.buffer.unshift(...events);
226
+ this.restartAutoFlush(this.flushBackoff);
227
+ return;
228
+ }
229
+ if (this.config.debug) {
230
+ console.warn('[GuardVideo EventTracker] Flush failed:', resp.status);
231
+ }
232
+ }
233
+ else {
234
+ if (this.flushBackoff > 0) {
235
+ this.flushBackoff = 0;
236
+ this.restartAutoFlush(this.config.flushIntervalMs);
237
+ }
191
238
  }
192
239
  }
193
240
  catch (err) {
@@ -403,6 +450,11 @@ class GuardVideoPlayer {
403
450
  this._onSecurityEvent = null;
404
451
  this.playlistNonce = null;
405
452
  this.nonceRefreshInProgress = false;
453
+ this.nonceRefreshTimer = null;
454
+ this.retryBackoff = 1000;
455
+ this.MAX_BACKOFF = 30000;
456
+ this.rateLimitCooldownUntil = 0;
457
+ this.nonceRefreshPromise = null;
406
458
  this._onRateChange = this.enforceMaxRate.bind(this);
407
459
  this.videoElement = videoElement;
408
460
  this.config = {
@@ -613,9 +665,18 @@ class GuardVideoPlayer {
613
665
  async solveChallenge(tokenId) {
614
666
  if (!this.config.apiBaseUrl)
615
667
  return undefined;
668
+ if (Date.now() < this.rateLimitCooldownUntil) {
669
+ this.log('Challenge skipped — rate limit cooldown active');
670
+ return undefined;
671
+ }
616
672
  try {
617
673
  const url = this.config.apiBaseUrl + '/videos/challenge/' + encodeURIComponent(tokenId);
618
674
  const resp = await fetch(url, { credentials: 'omit' });
675
+ if (resp.status === 429) {
676
+ this.enterRateLimitCooldown();
677
+ this.log('Challenge hit 429 — entering cooldown');
678
+ return undefined;
679
+ }
619
680
  if (!resp.ok) {
620
681
  this.log('Challenge fetch failed (challenge may be disabled)', resp.status);
621
682
  return undefined;
@@ -641,6 +702,10 @@ class GuardVideoPlayer {
641
702
  async acquireNonce(tokenId) {
642
703
  if (!this.config.apiBaseUrl)
643
704
  return null;
705
+ if (Date.now() < this.rateLimitCooldownUntil) {
706
+ this.log('Nonce acquisition skipped — rate limit cooldown active');
707
+ return this.playlistNonce;
708
+ }
644
709
  try {
645
710
  const challengeResult = await this.solveChallenge(tokenId);
646
711
  const url = this.config.apiBaseUrl + '/videos/playlist-session';
@@ -654,12 +719,19 @@ class GuardVideoPlayer {
654
719
  body: JSON.stringify(body),
655
720
  credentials: 'omit',
656
721
  });
722
+ if (resp.status === 429) {
723
+ this.enterRateLimitCooldown();
724
+ this.log('Nonce acquisition hit 429 — entering cooldown');
725
+ return this.playlistNonce;
726
+ }
657
727
  if (!resp.ok) {
658
728
  this.log('Playlist session nonce acquisition failed', resp.status);
659
729
  return null;
660
730
  }
661
731
  const data = await resp.json();
662
732
  this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
733
+ this.retryBackoff = 1000;
734
+ this.scheduleNonceRefresh(data.expiresIn);
663
735
  return data.nonce;
664
736
  }
665
737
  catch (err) {
@@ -667,19 +739,38 @@ class GuardVideoPlayer {
667
739
  return null;
668
740
  }
669
741
  }
742
+ scheduleNonceRefresh(expiresInSeconds) {
743
+ if (this.nonceRefreshTimer)
744
+ clearTimeout(this.nonceRefreshTimer);
745
+ const refreshMs = Math.max(5000, expiresInSeconds * 750);
746
+ this.nonceRefreshTimer = setTimeout(() => {
747
+ this.refreshNonce().catch(() => {
748
+ this.log('Proactive nonce refresh failed');
749
+ });
750
+ }, refreshMs);
751
+ }
670
752
  async refreshNonce() {
671
- if (this.nonceRefreshInProgress)
672
- return;
753
+ if (this.nonceRefreshInProgress && this.nonceRefreshPromise) {
754
+ return this.nonceRefreshPromise;
755
+ }
673
756
  if (!this.embedToken)
674
757
  return;
675
758
  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
- }
759
+ this.nonceRefreshPromise = (async () => {
760
+ try {
761
+ const nonce = await this.acquireNonce(this.embedToken.tokenId);
762
+ this.playlistNonce = nonce;
763
+ }
764
+ finally {
765
+ this.nonceRefreshInProgress = false;
766
+ this.nonceRefreshPromise = null;
767
+ }
768
+ })();
769
+ return this.nonceRefreshPromise;
770
+ }
771
+ enterRateLimitCooldown() {
772
+ this.rateLimitCooldownUntil = Date.now() + this.retryBackoff;
773
+ this.retryBackoff = Math.min(this.retryBackoff * 2, this.MAX_BACKOFF);
683
774
  }
684
775
  async initializePlayer() {
685
776
  if (!this.embedToken) {
@@ -719,8 +810,12 @@ class GuardVideoPlayer {
719
810
  enableWorker: true,
720
811
  lowLatencyMode: false,
721
812
  liveSyncDurationCount: 3,
722
- manifestLoadingMaxRetry: 6,
723
- levelLoadingMaxRetry: 6,
813
+ manifestLoadingMaxRetry: 3,
814
+ manifestLoadingRetryDelay: 2000,
815
+ levelLoadingMaxRetry: 3,
816
+ levelLoadingRetryDelay: 2000,
817
+ fragLoadingMaxRetry: 4,
818
+ fragLoadingRetryDelay: 1000,
724
819
  xhrSetup(xhr, url) {
725
820
  if (url.includes('/watermark-stream/') && url.includes('.m3u8')) {
726
821
  if (self.playlistNonce) {
@@ -728,10 +823,6 @@ class GuardVideoPlayer {
728
823
  const nonceUrl = url + separator + 'nonce=' + encodeURIComponent(self.playlistNonce);
729
824
  xhr.open('GET', nonceUrl, true);
730
825
  self.log('Injected nonce into playlist request');
731
- self.playlistNonce = null;
732
- self.refreshNonce().catch(() => {
733
- self.log('Background nonce refresh failed');
734
- });
735
826
  }
736
827
  }
737
828
  },
@@ -743,6 +834,7 @@ class GuardVideoPlayer {
743
834
  this.log('HLS manifest parsed', { levels: data.levels.map((l) => l.height + 'p') });
744
835
  this.setState(exports.PlayerState.READY);
745
836
  this.config.onReady();
837
+ this.retryBackoff = 1000;
746
838
  if (this.config.autoplay)
747
839
  this.play();
748
840
  });
@@ -758,18 +850,52 @@ class GuardVideoPlayer {
758
850
  this.currentQuality = quality;
759
851
  this.log('Quality switched to ' + quality.name);
760
852
  this.config.onQualityChange(quality.name);
853
+ this.eventTracker?.track('quality_change', this.videoElement.currentTime, {
854
+ level: data.level,
855
+ height: level.height,
856
+ bitrate: level.bitrate,
857
+ });
761
858
  });
762
859
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
763
860
  this.error('HLS Error', data);
861
+ this.eventTracker?.track('error', this.videoElement.currentTime, {
862
+ type: data.type,
863
+ details: data.details,
864
+ fatal: data.fatal,
865
+ });
866
+ if (Date.now() < this.rateLimitCooldownUntil) {
867
+ this.log('Suppressing retry — rate limit cooldown active (' +
868
+ Math.ceil((this.rateLimitCooldownUntil - Date.now()) / 1000) + 's remaining)');
869
+ if (data.fatal) {
870
+ const delay = this.rateLimitCooldownUntil - Date.now() + 500;
871
+ setTimeout(() => this.hls?.startLoad(), delay);
872
+ }
873
+ return;
874
+ }
875
+ const httpStatus = data.response?.code;
876
+ if (httpStatus === 429) {
877
+ this.enterRateLimitCooldown();
878
+ this.log('429 detected — entering cooldown for ' + this.retryBackoff + 'ms');
879
+ setTimeout(() => {
880
+ if (this.embedToken?.forensicWatermark) {
881
+ this.refreshNonce().then(() => this.hls?.startLoad()).catch(() => this.hls?.startLoad());
882
+ }
883
+ else {
884
+ this.hls?.startLoad();
885
+ }
886
+ }, this.retryBackoff);
887
+ return;
888
+ }
764
889
  if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
765
890
  data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
766
891
  this.embedToken?.forensicWatermark) {
767
892
  this.log('Playlist load failed — refreshing nonce before retry');
768
893
  this.refreshNonce().then(() => {
769
- setTimeout(() => this.hls?.startLoad(), 500);
894
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
770
895
  }).catch(() => {
771
- setTimeout(() => this.hls?.startLoad(), 1000);
896
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
772
897
  });
898
+ this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
773
899
  return;
774
900
  }
775
901
  if (data.fatal) {
@@ -779,7 +905,8 @@ class GuardVideoPlayer {
779
905
  if (this.embedToken?.forensicWatermark) {
780
906
  this.refreshNonce().catch(() => { });
781
907
  }
782
- setTimeout(() => this.hls?.startLoad(), 1000);
908
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
909
+ this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
783
910
  break;
784
911
  case Hls.ErrorTypes.MEDIA_ERROR:
785
912
  this.error('Media error, attempting recovery...');
@@ -857,6 +984,10 @@ class GuardVideoPlayer {
857
984
  getState() { return this.state; }
858
985
  destroy() {
859
986
  this.log('Destroying player');
987
+ if (this.nonceRefreshTimer) {
988
+ clearTimeout(this.nonceRefreshTimer);
989
+ this.nonceRefreshTimer = null;
990
+ }
860
991
  if (this.eventTracker) {
861
992
  this.eventTracker.destroy();
862
993
  this.eventTracker = null;