@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/core/player.d.ts +6 -0
- package/dist/core/player.d.ts.map +1 -1
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.esm.js +116 -13
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +116 -13
- package/dist/index.js.map +1 -1
- package/dist/react/GuardVideoPlayer.d.ts.map +1 -1
- package/dist/ui/PlayerUI.d.ts +5 -0
- package/dist/ui/PlayerUI.d.ts.map +1 -1
- package/dist/vanilla/guardvideo-player.js +115 -13
- package/dist/vanilla/guardvideo-player.js.map +1 -1
- package/dist/vanilla/guardvideo-player.min.js +1 -1
- package/dist/vanilla/guardvideo-player.min.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
928
|
+
this.scheduleRetry(() => this.hls?.startLoad(), this.retryBackoff);
|
|
895
929
|
}).catch(() => {
|
|
896
|
-
|
|
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.
|
|
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
|
-
|
|
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', () =>
|
|
924
|
-
|
|
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.
|
|
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,
|