@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/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 +154 -23
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +154 -23
- package/dist/index.js.map +1 -1
- package/dist/vanilla/guardvideo-player.js +154 -23
- 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,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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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:
|
|
723
|
-
|
|
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(),
|
|
894
|
+
setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
|
|
770
895
|
}).catch(() => {
|
|
771
|
-
setTimeout(() => this.hls?.startLoad(),
|
|
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(),
|
|
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;
|