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