@guardvideo/player-sdk 3.5.0 → 3.7.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
@@ -455,6 +455,10 @@ class GuardVideoPlayer {
455
455
  this.MAX_BACKOFF = 30000;
456
456
  this.rateLimitCooldownUntil = 0;
457
457
  this.nonceRefreshPromise = null;
458
+ this.destroyed = false;
459
+ this.networkRetryCount = 0;
460
+ this.MAX_NETWORK_RETRIES = 6;
461
+ this.pendingRetryTimer = null;
458
462
  this._onRateChange = this.enforceMaxRate.bind(this);
459
463
  this.videoElement = videoElement;
460
464
  this.config = {
@@ -731,6 +735,7 @@ class GuardVideoPlayer {
731
735
  const data = await resp.json();
732
736
  this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
733
737
  this.retryBackoff = 1000;
738
+ this.networkRetryCount = 0;
734
739
  this.scheduleNonceRefresh(data.expiresIn);
735
740
  return data.nonce;
736
741
  }
@@ -744,6 +749,8 @@ class GuardVideoPlayer {
744
749
  clearTimeout(this.nonceRefreshTimer);
745
750
  const refreshMs = Math.max(5000, expiresInSeconds * 750);
746
751
  this.nonceRefreshTimer = setTimeout(() => {
752
+ if (this.destroyed)
753
+ return;
747
754
  this.refreshNonce().catch(() => {
748
755
  this.log('Proactive nonce refresh failed');
749
756
  });
@@ -772,6 +779,24 @@ class GuardVideoPlayer {
772
779
  this.rateLimitCooldownUntil = Date.now() + this.retryBackoff;
773
780
  this.retryBackoff = Math.min(this.retryBackoff * 2, this.MAX_BACKOFF);
774
781
  }
782
+ scheduleRetry(fn, delayMs) {
783
+ if (this.pendingRetryTimer)
784
+ clearTimeout(this.pendingRetryTimer);
785
+ this.pendingRetryTimer = setTimeout(() => {
786
+ this.pendingRetryTimer = null;
787
+ if (!this.destroyed && this.hls)
788
+ fn();
789
+ }, delayMs);
790
+ }
791
+ stopLoading() {
792
+ if (this.pendingRetryTimer) {
793
+ clearTimeout(this.pendingRetryTimer);
794
+ this.pendingRetryTimer = null;
795
+ }
796
+ this.hls?.stopLoad();
797
+ this.networkRetryCount = 0;
798
+ this.retryBackoff = 1000;
799
+ }
775
800
  async initializePlayer() {
776
801
  if (!this.embedToken) {
777
802
  throw new Error('No embed token available');
@@ -835,6 +860,7 @@ class GuardVideoPlayer {
835
860
  this.setState(exports.PlayerState.READY);
836
861
  this.config.onReady();
837
862
  this.retryBackoff = 1000;
863
+ this.networkRetryCount = 0;
838
864
  if (this.config.autoplay)
839
865
  this.play();
840
866
  });
@@ -863,12 +889,14 @@ class GuardVideoPlayer {
863
889
  details: data.details,
864
890
  fatal: data.fatal,
865
891
  });
892
+ if (this.destroyed)
893
+ return;
866
894
  if (Date.now() < this.rateLimitCooldownUntil) {
867
895
  this.log('Suppressing retry — rate limit cooldown active (' +
868
896
  Math.ceil((this.rateLimitCooldownUntil - Date.now()) / 1000) + 's remaining)');
869
897
  if (data.fatal) {
870
898
  const delay = this.rateLimitCooldownUntil - Date.now() + 500;
871
- setTimeout(() => this.hls?.startLoad(), delay);
899
+ this.scheduleRetry(() => this.hls?.startLoad(), delay);
872
900
  }
873
901
  return;
874
902
  }
@@ -876,7 +904,7 @@ class GuardVideoPlayer {
876
904
  if (httpStatus === 429) {
877
905
  this.enterRateLimitCooldown();
878
906
  this.log('429 detected — entering cooldown for ' + this.retryBackoff + 'ms');
879
- setTimeout(() => {
907
+ this.scheduleRetry(() => {
880
908
  if (this.embedToken?.forensicWatermark) {
881
909
  this.refreshNonce().then(() => this.hls?.startLoad()).catch(() => this.hls?.startLoad());
882
910
  }
@@ -889,11 +917,17 @@ class GuardVideoPlayer {
889
917
  if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
890
918
  data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
891
919
  this.embedToken?.forensicWatermark) {
892
- this.log('Playlist load failed — refreshing nonce before retry');
920
+ this.networkRetryCount++;
921
+ if (this.networkRetryCount > this.MAX_NETWORK_RETRIES) {
922
+ this.error('Max network retries exceeded for playlist load');
923
+ this.handleError({ code: 'NETWORK_ERROR', message: 'Playlist load failed after multiple retries', fatal: true, details: data });
924
+ return;
925
+ }
926
+ this.log('Playlist load failed — refreshing nonce before retry (' + this.networkRetryCount + '/' + this.MAX_NETWORK_RETRIES + ')');
893
927
  this.refreshNonce().then(() => {
894
- setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
928
+ this.scheduleRetry(() => this.hls?.startLoad(), this.retryBackoff);
895
929
  }).catch(() => {
896
- setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
930
+ this.scheduleRetry(() => this.hls?.startLoad(), this.retryBackoff);
897
931
  });
898
932
  this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
899
933
  return;
@@ -901,11 +935,17 @@ class GuardVideoPlayer {
901
935
  if (data.fatal) {
902
936
  switch (data.type) {
903
937
  case Hls.ErrorTypes.NETWORK_ERROR:
904
- this.error('Network error, attempting recovery...');
938
+ this.networkRetryCount++;
939
+ if (this.networkRetryCount > this.MAX_NETWORK_RETRIES) {
940
+ this.error('Max network retries exceeded — giving up');
941
+ this.handleError({ code: 'NETWORK_ERROR', message: 'Network error after multiple retries. Please check your connection.', fatal: true, details: data });
942
+ return;
943
+ }
944
+ this.error('Network error, attempting recovery (' + this.networkRetryCount + '/' + this.MAX_NETWORK_RETRIES + ')...');
905
945
  if (this.embedToken?.forensicWatermark) {
906
946
  this.refreshNonce().catch(() => { });
907
947
  }
908
- setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
948
+ this.scheduleRetry(() => this.hls?.startLoad(), this.retryBackoff);
909
949
  this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
910
950
  break;
911
951
  case Hls.ErrorTypes.MEDIA_ERROR:
@@ -920,8 +960,21 @@ class GuardVideoPlayer {
920
960
  this.setupVideoEventListeners();
921
961
  }
922
962
  setupVideoEventListeners() {
923
- this.videoElement.addEventListener('playing', () => this.setState(exports.PlayerState.PLAYING));
924
- this.videoElement.addEventListener('pause', () => this.setState(exports.PlayerState.PAUSED));
963
+ this.videoElement.addEventListener('playing', () => {
964
+ this.setState(exports.PlayerState.PLAYING);
965
+ if (this.hls)
966
+ this.hls.startLoad(-1);
967
+ });
968
+ this.videoElement.addEventListener('pause', () => {
969
+ this.setState(exports.PlayerState.PAUSED);
970
+ if (this.hls && !this.videoElement.seeking) {
971
+ this.hls.stopLoad();
972
+ if (this.pendingRetryTimer) {
973
+ clearTimeout(this.pendingRetryTimer);
974
+ this.pendingRetryTimer = null;
975
+ }
976
+ }
977
+ });
925
978
  this.videoElement.addEventListener('waiting', () => this.setState(exports.PlayerState.BUFFERING));
926
979
  this.videoElement.addEventListener('error', () => {
927
980
  const error = this.videoElement.error;
@@ -948,6 +1001,8 @@ class GuardVideoPlayer {
948
1001
  }
949
1002
  async play() {
950
1003
  try {
1004
+ this.networkRetryCount = 0;
1005
+ this.retryBackoff = 1000;
951
1006
  await this.videoElement.play();
952
1007
  }
953
1008
  catch (err) {
@@ -984,6 +1039,11 @@ class GuardVideoPlayer {
984
1039
  getState() { return this.state; }
985
1040
  destroy() {
986
1041
  this.log('Destroying player');
1042
+ this.destroyed = true;
1043
+ if (this.pendingRetryTimer) {
1044
+ clearTimeout(this.pendingRetryTimer);
1045
+ this.pendingRetryTimer = null;
1046
+ }
987
1047
  if (this.nonceRefreshTimer) {
988
1048
  clearTimeout(this.nonceRefreshTimer);
989
1049
  this.nonceRefreshTimer = null;
@@ -1817,6 +1877,7 @@ class PlayerUI {
1817
1877
  this._ctxTargetBound = () => { };
1818
1878
  this._ctxKeyDownBound = () => { };
1819
1879
  this._watermarkObserver = null;
1880
+ this._watermarkStylePollTimer = null;
1820
1881
  this._watermarkText = '';
1821
1882
  this._watermarkDriftTimer = null;
1822
1883
  const accent = config.branding?.accentColor ?? '#00e5a0';
@@ -2494,6 +2555,7 @@ class PlayerUI {
2494
2555
  tampered = true;
2495
2556
  }
2496
2557
  if (m.type === 'attributes' && m.target === this.watermarkCanvas) {
2558
+ this.watermarkCanvas.removeAttribute('style');
2497
2559
  this.watermarkCanvas.className = 'gvp-watermark-canvas';
2498
2560
  this.watermarkCanvas.setAttribute('aria-hidden', 'true');
2499
2561
  this._renderCanvasWatermark(this._watermarkText);
@@ -2507,14 +2569,51 @@ class PlayerUI {
2507
2569
  }
2508
2570
  }
2509
2571
  if (tampered) {
2510
- this.root.dispatchEvent(new CustomEvent('gv:security', {
2511
- bubbles: true,
2512
- detail: { type: 'watermark_tamper', timestamp: Date.now() },
2513
- }));
2572
+ this._onWatermarkTamper();
2514
2573
  }
2515
2574
  });
2516
2575
  this._watermarkObserver.observe(this.root, { childList: true });
2517
2576
  this._watermarkObserver.observe(this.watermarkDiv, { attributes: true, childList: true });
2577
+ this._watermarkObserver.observe(this.watermarkCanvas, { attributes: true });
2578
+ if (this._watermarkStylePollTimer)
2579
+ clearInterval(this._watermarkStylePollTimer);
2580
+ this._watermarkStylePollTimer = setInterval(() => {
2581
+ if (!this._watermarkText)
2582
+ return;
2583
+ const divHidden = this._isElementHidden(this.watermarkDiv);
2584
+ const canvasHidden = this._isElementHidden(this.watermarkCanvas);
2585
+ if (divHidden || canvasHidden) {
2586
+ this.watermarkDiv.removeAttribute('style');
2587
+ this.watermarkDiv.className = 'gvp-watermark';
2588
+ this.watermarkCanvas.removeAttribute('style');
2589
+ this.watermarkCanvas.className = 'gvp-watermark-canvas';
2590
+ this._purgeWatermarkOverrideStyles();
2591
+ this._onWatermarkTamper();
2592
+ }
2593
+ }, 500);
2594
+ }
2595
+ _isElementHidden(el) {
2596
+ const cs = getComputedStyle(el);
2597
+ return (cs.display === 'none' ||
2598
+ cs.visibility === 'hidden' ||
2599
+ parseFloat(cs.opacity) < 0.01 ||
2600
+ (el.offsetWidth === 0 && el.offsetHeight === 0));
2601
+ }
2602
+ _purgeWatermarkOverrideStyles() {
2603
+ const styles = this.root.querySelectorAll('style');
2604
+ styles.forEach((s) => {
2605
+ const text = s.textContent ?? '';
2606
+ if (/gvp-watermark/i.test(text)) {
2607
+ s.remove();
2608
+ }
2609
+ });
2610
+ }
2611
+ _onWatermarkTamper() {
2612
+ this.corePlayer.pause();
2613
+ this.root.dispatchEvent(new CustomEvent('gv:security', {
2614
+ bubbles: true,
2615
+ detail: { type: 'watermark_tamper', timestamp: Date.now() },
2616
+ }));
2518
2617
  }
2519
2618
  _refillWatermark() {
2520
2619
  const seed = this._hashCode(this._watermarkText);
@@ -2698,6 +2797,7 @@ class PlayerUI {
2698
2797
  }
2699
2798
  play() { return this.corePlayer.play(); }
2700
2799
  pause() { return this.corePlayer.pause(); }
2800
+ stopLoading() { return this.corePlayer.stopLoading(); }
2701
2801
  seek(t) { return this.corePlayer.seek(t); }
2702
2802
  getCurrentTime() { return this.corePlayer.getCurrentTime(); }
2703
2803
  getDuration() { return this.corePlayer.getDuration(); }
@@ -2725,6 +2825,8 @@ class PlayerUI {
2725
2825
  this._watermarkObserver?.disconnect();
2726
2826
  if (this._watermarkDriftTimer)
2727
2827
  clearInterval(this._watermarkDriftTimer);
2828
+ if (this._watermarkStylePollTimer)
2829
+ clearInterval(this._watermarkStylePollTimer);
2728
2830
  this.corePlayer.destroy();
2729
2831
  this.root.remove();
2730
2832
  }
@@ -2757,6 +2859,7 @@ const GuardVideoPlayerComponent = React.forwardRef((props, ref) => {
2757
2859
  React.useImperativeHandle(ref, () => ({
2758
2860
  play: () => uiRef.current?.play() ?? Promise.resolve(),
2759
2861
  pause: () => uiRef.current?.pause(),
2862
+ stopLoading: () => uiRef.current?.stopLoading(),
2760
2863
  seek: (t) => uiRef.current?.seek(t),
2761
2864
  getCurrentTime: () => uiRef.current?.getCurrentTime() ?? 0,
2762
2865
  getDuration: () => uiRef.current?.getDuration() ?? 0,