@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
|
@@ -21,6 +21,8 @@ export declare class EventTracker {
|
|
|
21
21
|
private accumulator;
|
|
22
22
|
private destroyed;
|
|
23
23
|
private _onBeforeUnload;
|
|
24
|
+
private flushBackoff;
|
|
25
|
+
private flushing;
|
|
24
26
|
constructor(config: EventTrackerConfig);
|
|
25
27
|
track(type: string, positionSeconds?: number, payload?: Record<string, unknown>): void;
|
|
26
28
|
startChunk(position: number): void;
|
|
@@ -30,6 +32,7 @@ export declare class EventTracker {
|
|
|
30
32
|
flush(): Promise<void>;
|
|
31
33
|
destroy(): void;
|
|
32
34
|
private startAutoFlush;
|
|
35
|
+
private restartAutoFlush;
|
|
33
36
|
private hookPageUnload;
|
|
34
37
|
private sendBatch;
|
|
35
38
|
private randomNonce;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventTracker.d.ts","sourceRoot":"","sources":["../../src/core/EventTracker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EventTracker.d.ts","sourceRoot":"","sources":["../../src/core/EventTracker.ts"],"names":[],"mappings":"AAiCA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAEjC,QAAQ,EAAE,MAAM,CAAC;IAEjB,OAAO,EAAE,MAAM,CAAC;IAEhB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,YAAY,EAAE,MAAM,CAAC;IAErB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,eAAe,CAA6B;IAEpD,OAAO,CAAC,YAAY,CAAK;IAEzB,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,kBAAkB;IAoBtC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAqBtF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKlC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKjC,QAAQ,IAAI,IAAI;IAKhB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAK/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB5B,OAAO,IAAI,IAAI;IAYf,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,cAAc;YA+BR,SAAS;IA4DvB,OAAO,CAAC,WAAW;YASL,IAAI;CAenB"}
|
package/dist/core/player.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export declare class GuardVideoPlayer implements PlayerInstance {
|
|
|
13
13
|
private sessionId;
|
|
14
14
|
private playlistNonce;
|
|
15
15
|
private nonceRefreshInProgress;
|
|
16
|
+
private nonceRefreshTimer;
|
|
17
|
+
private retryBackoff;
|
|
18
|
+
private readonly MAX_BACKOFF;
|
|
19
|
+
private rateLimitCooldownUntil;
|
|
20
|
+
private nonceRefreshPromise;
|
|
16
21
|
private _onRateChange;
|
|
17
22
|
constructor(videoElement: HTMLVideoElement, videoId: string, config: PlayerConfig);
|
|
18
23
|
private log;
|
|
@@ -27,7 +32,9 @@ export declare class GuardVideoPlayer implements PlayerInstance {
|
|
|
27
32
|
private fetchEmbedToken;
|
|
28
33
|
private solveChallenge;
|
|
29
34
|
private acquireNonce;
|
|
35
|
+
private scheduleNonceRefresh;
|
|
30
36
|
private refreshNonce;
|
|
37
|
+
private enterRateLimitCooldown;
|
|
31
38
|
private initializePlayer;
|
|
32
39
|
private initializeHls;
|
|
33
40
|
private setupVideoEventListeners;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../../src/core/player.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,YAAY,EAEZ,WAAW,EACX,cAAc,EACd,YAAY,EAIb,MAAM,SAAS,CAAC;AA8BjB,qBAAa,gBAAiB,YAAW,cAAc;
|
|
1
|
+
{"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../../src/core/player.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,YAAY,EAEZ,WAAW,EACX,cAAc,EACd,YAAY,EAIb,MAAM,SAAS,CAAC;AA8BjB,qBAAa,gBAAiB,YAAW,cAAc;IA+BnD,OAAO,CAAC,OAAO;IA9BjB,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,UAAU,CAAmC;IACrD,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,SAAS,CAAqB;IAEtC,OAAO,CAAC,aAAa,CAAuB;IAE5C,OAAO,CAAC,sBAAsB,CAAS;IAEvC,OAAO,CAAC,iBAAiB,CAA8C;IAEvE,OAAO,CAAC,YAAY,CAAQ;IAE5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,OAAO,CAAC,sBAAsB,CAAK;IAEnC,OAAO,CAAC,mBAAmB,CAA8B;IAGzD,OAAO,CAAC,aAAa,CAAkC;gBAGrD,YAAY,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,EACvB,MAAM,EAAE,YAAY;IAqDtB,OAAO,CAAC,GAAG;IAMX,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,kBAAkB;IA2B1B,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,cAAc;YAWR,UAAU;YAkCV,sBAAsB;IA6CpC,OAAO,CAAC,gBAAgB;YA0CV,eAAe;YAgDf,cAAc;YAmEd,YAAY;IAqD1B,OAAO,CAAC,oBAAoB;YAed,YAAY;IAmB1B,OAAO,CAAC,sBAAsB;YAKhB,gBAAgB;IAiC9B,OAAO,CAAC,aAAa;IA6IrB,OAAO,CAAC,wBAAwB;IAuBhC,OAAO,CAAC,WAAW;IASN,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B,KAAK,IAAI,IAAI;IAEb,cAAc,IAAI,MAAM;IAExB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAExB,WAAW,IAAI,MAAM;IAErB,SAAS,IAAI,MAAM;IAEnB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,gBAAgB,IAAI,YAAY,EAAE;IAWlC,iBAAiB,IAAI,YAAY,GAAG,IAAI;IAExC,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOpC,QAAQ,IAAI,WAAW;IAEvB,OAAO,IAAI,IAAI;CAgCvB"}
|
package/dist/index.esm.js
CHANGED
|
@@ -68,8 +68,19 @@ class WatchChunkAccumulator {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
const VALID_EVENT_TYPES = [
|
|
72
|
+
'watch_chunk',
|
|
73
|
+
'play',
|
|
74
|
+
'pause',
|
|
75
|
+
'seek',
|
|
76
|
+
'ended',
|
|
77
|
+
'quality_change',
|
|
78
|
+
'error',
|
|
79
|
+
'security_event',
|
|
80
|
+
];
|
|
71
81
|
const MAX_BATCH_SIZE = 25;
|
|
72
82
|
const DEFAULT_FLUSH_INTERVAL = 5000;
|
|
83
|
+
const MAX_FLUSH_BACKOFF = 60000;
|
|
73
84
|
class EventTracker {
|
|
74
85
|
constructor(config) {
|
|
75
86
|
this.buffer = [];
|
|
@@ -77,6 +88,8 @@ class EventTracker {
|
|
|
77
88
|
this.accumulator = new WatchChunkAccumulator();
|
|
78
89
|
this.destroyed = false;
|
|
79
90
|
this._onBeforeUnload = null;
|
|
91
|
+
this.flushBackoff = 0;
|
|
92
|
+
this.flushing = false;
|
|
80
93
|
this.config = {
|
|
81
94
|
endpoint: config.endpoint,
|
|
82
95
|
tokenId: config.tokenId,
|
|
@@ -92,6 +105,12 @@ class EventTracker {
|
|
|
92
105
|
track(type, positionSeconds, payload) {
|
|
93
106
|
if (this.destroyed)
|
|
94
107
|
return;
|
|
108
|
+
if (!VALID_EVENT_TYPES.includes(type)) {
|
|
109
|
+
if (this.config.debug) {
|
|
110
|
+
console.warn('[GuardVideo EventTracker] Skipping unknown event type:', type);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
95
114
|
this.buffer.push({
|
|
96
115
|
event_type: type,
|
|
97
116
|
event_at: new Date().toISOString(),
|
|
@@ -115,7 +134,7 @@ class EventTracker {
|
|
|
115
134
|
this.config.sessionId = sessionId;
|
|
116
135
|
}
|
|
117
136
|
async flush() {
|
|
118
|
-
if (this.destroyed)
|
|
137
|
+
if (this.destroyed || this.flushing)
|
|
119
138
|
return;
|
|
120
139
|
const chunks = this.accumulator.drain();
|
|
121
140
|
for (const c of chunks) {
|
|
@@ -123,8 +142,14 @@ class EventTracker {
|
|
|
123
142
|
}
|
|
124
143
|
if (this.buffer.length === 0)
|
|
125
144
|
return;
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
this.flushing = true;
|
|
146
|
+
try {
|
|
147
|
+
const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
|
|
148
|
+
await this.sendBatch(batch);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
this.flushing = false;
|
|
152
|
+
}
|
|
128
153
|
}
|
|
129
154
|
destroy() {
|
|
130
155
|
this.destroyed = true;
|
|
@@ -139,6 +164,11 @@ class EventTracker {
|
|
|
139
164
|
startAutoFlush() {
|
|
140
165
|
this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
|
|
141
166
|
}
|
|
167
|
+
restartAutoFlush(intervalMs) {
|
|
168
|
+
if (this.flushTimer)
|
|
169
|
+
clearInterval(this.flushTimer);
|
|
170
|
+
this.flushTimer = setInterval(() => this.flush(), intervalMs);
|
|
171
|
+
}
|
|
142
172
|
hookPageUnload() {
|
|
143
173
|
if (typeof window === 'undefined')
|
|
144
174
|
return;
|
|
@@ -184,8 +214,25 @@ class EventTracker {
|
|
|
184
214
|
body,
|
|
185
215
|
credentials: 'omit',
|
|
186
216
|
});
|
|
187
|
-
if (!resp.ok
|
|
188
|
-
|
|
217
|
+
if (!resp.ok) {
|
|
218
|
+
if (resp.status === 429) {
|
|
219
|
+
this.flushBackoff = Math.min((this.flushBackoff || this.config.flushIntervalMs) * 2, MAX_FLUSH_BACKOFF);
|
|
220
|
+
if (this.config.debug) {
|
|
221
|
+
console.warn('[GuardVideo EventTracker] 429 — backing off to', this.flushBackoff, 'ms');
|
|
222
|
+
}
|
|
223
|
+
this.buffer.unshift(...events);
|
|
224
|
+
this.restartAutoFlush(this.flushBackoff);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (this.config.debug) {
|
|
228
|
+
console.warn('[GuardVideo EventTracker] Flush failed:', resp.status);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
if (this.flushBackoff > 0) {
|
|
233
|
+
this.flushBackoff = 0;
|
|
234
|
+
this.restartAutoFlush(this.config.flushIntervalMs);
|
|
235
|
+
}
|
|
189
236
|
}
|
|
190
237
|
}
|
|
191
238
|
catch (err) {
|
|
@@ -401,6 +448,11 @@ class GuardVideoPlayer {
|
|
|
401
448
|
this._onSecurityEvent = null;
|
|
402
449
|
this.playlistNonce = null;
|
|
403
450
|
this.nonceRefreshInProgress = false;
|
|
451
|
+
this.nonceRefreshTimer = null;
|
|
452
|
+
this.retryBackoff = 1000;
|
|
453
|
+
this.MAX_BACKOFF = 30000;
|
|
454
|
+
this.rateLimitCooldownUntil = 0;
|
|
455
|
+
this.nonceRefreshPromise = null;
|
|
404
456
|
this._onRateChange = this.enforceMaxRate.bind(this);
|
|
405
457
|
this.videoElement = videoElement;
|
|
406
458
|
this.config = {
|
|
@@ -611,27 +663,34 @@ class GuardVideoPlayer {
|
|
|
611
663
|
async solveChallenge(tokenId) {
|
|
612
664
|
if (!this.config.apiBaseUrl)
|
|
613
665
|
return undefined;
|
|
666
|
+
if (Date.now() < this.rateLimitCooldownUntil) {
|
|
667
|
+
this.log('Challenge skipped — rate limit cooldown active');
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
614
670
|
try {
|
|
615
671
|
const url = this.config.apiBaseUrl + '/videos/challenge/' + encodeURIComponent(tokenId);
|
|
616
672
|
const resp = await fetch(url, { credentials: 'omit' });
|
|
673
|
+
if (resp.status === 429) {
|
|
674
|
+
this.enterRateLimitCooldown();
|
|
675
|
+
this.log('Challenge hit 429 — entering cooldown');
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
617
678
|
if (!resp.ok) {
|
|
618
679
|
this.log('Challenge fetch failed (challenge may be disabled)', resp.status);
|
|
619
680
|
return undefined;
|
|
620
681
|
}
|
|
621
682
|
const challenge = await resp.json();
|
|
622
|
-
this.log('Challenge received, computing PBKDF2
|
|
683
|
+
this.log('Challenge received, computing PBKDF2 proof-of-work...');
|
|
623
684
|
const encoder = new TextEncoder();
|
|
624
685
|
const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(challenge.nonce), 'PBKDF2', false, ['deriveBits']);
|
|
625
|
-
|
|
686
|
+
await crypto.subtle.deriveBits({
|
|
626
687
|
name: 'PBKDF2',
|
|
627
688
|
salt: encoder.encode(challenge.salt),
|
|
628
689
|
iterations: challenge.iterations,
|
|
629
690
|
hash: 'SHA-256',
|
|
630
691
|
}, keyMaterial, challenge.keyLength * 8);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
this.log('Challenge response computed');
|
|
634
|
-
return hexResponse;
|
|
692
|
+
this.log('Challenge proof-of-work completed');
|
|
693
|
+
return { nonce: challenge.nonce };
|
|
635
694
|
}
|
|
636
695
|
catch (err) {
|
|
637
696
|
this.log('Challenge computation failed (non-fatal):', err);
|
|
@@ -641,12 +700,16 @@ class GuardVideoPlayer {
|
|
|
641
700
|
async acquireNonce(tokenId) {
|
|
642
701
|
if (!this.config.apiBaseUrl)
|
|
643
702
|
return null;
|
|
703
|
+
if (Date.now() < this.rateLimitCooldownUntil) {
|
|
704
|
+
this.log('Nonce acquisition skipped — rate limit cooldown active');
|
|
705
|
+
return this.playlistNonce;
|
|
706
|
+
}
|
|
644
707
|
try {
|
|
645
|
-
const
|
|
708
|
+
const challengeResult = await this.solveChallenge(tokenId);
|
|
646
709
|
const url = this.config.apiBaseUrl + '/videos/playlist-session';
|
|
647
710
|
const body = { tokenId };
|
|
648
|
-
if (
|
|
649
|
-
body.
|
|
711
|
+
if (challengeResult) {
|
|
712
|
+
body.challengeNonce = challengeResult.nonce;
|
|
650
713
|
}
|
|
651
714
|
const resp = await fetch(url, {
|
|
652
715
|
method: 'POST',
|
|
@@ -654,12 +717,19 @@ class GuardVideoPlayer {
|
|
|
654
717
|
body: JSON.stringify(body),
|
|
655
718
|
credentials: 'omit',
|
|
656
719
|
});
|
|
720
|
+
if (resp.status === 429) {
|
|
721
|
+
this.enterRateLimitCooldown();
|
|
722
|
+
this.log('Nonce acquisition hit 429 — entering cooldown');
|
|
723
|
+
return this.playlistNonce;
|
|
724
|
+
}
|
|
657
725
|
if (!resp.ok) {
|
|
658
726
|
this.log('Playlist session nonce acquisition failed', resp.status);
|
|
659
727
|
return null;
|
|
660
728
|
}
|
|
661
729
|
const data = await resp.json();
|
|
662
730
|
this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
|
|
731
|
+
this.retryBackoff = 1000;
|
|
732
|
+
this.scheduleNonceRefresh(data.expiresIn);
|
|
663
733
|
return data.nonce;
|
|
664
734
|
}
|
|
665
735
|
catch (err) {
|
|
@@ -667,19 +737,38 @@ class GuardVideoPlayer {
|
|
|
667
737
|
return null;
|
|
668
738
|
}
|
|
669
739
|
}
|
|
740
|
+
scheduleNonceRefresh(expiresInSeconds) {
|
|
741
|
+
if (this.nonceRefreshTimer)
|
|
742
|
+
clearTimeout(this.nonceRefreshTimer);
|
|
743
|
+
const refreshMs = Math.max(5000, expiresInSeconds * 750);
|
|
744
|
+
this.nonceRefreshTimer = setTimeout(() => {
|
|
745
|
+
this.refreshNonce().catch(() => {
|
|
746
|
+
this.log('Proactive nonce refresh failed');
|
|
747
|
+
});
|
|
748
|
+
}, refreshMs);
|
|
749
|
+
}
|
|
670
750
|
async refreshNonce() {
|
|
671
|
-
if (this.nonceRefreshInProgress)
|
|
672
|
-
return;
|
|
751
|
+
if (this.nonceRefreshInProgress && this.nonceRefreshPromise) {
|
|
752
|
+
return this.nonceRefreshPromise;
|
|
753
|
+
}
|
|
673
754
|
if (!this.embedToken)
|
|
674
755
|
return;
|
|
675
756
|
this.nonceRefreshInProgress = true;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
757
|
+
this.nonceRefreshPromise = (async () => {
|
|
758
|
+
try {
|
|
759
|
+
const nonce = await this.acquireNonce(this.embedToken.tokenId);
|
|
760
|
+
this.playlistNonce = nonce;
|
|
761
|
+
}
|
|
762
|
+
finally {
|
|
763
|
+
this.nonceRefreshInProgress = false;
|
|
764
|
+
this.nonceRefreshPromise = null;
|
|
765
|
+
}
|
|
766
|
+
})();
|
|
767
|
+
return this.nonceRefreshPromise;
|
|
768
|
+
}
|
|
769
|
+
enterRateLimitCooldown() {
|
|
770
|
+
this.rateLimitCooldownUntil = Date.now() + this.retryBackoff;
|
|
771
|
+
this.retryBackoff = Math.min(this.retryBackoff * 2, this.MAX_BACKOFF);
|
|
683
772
|
}
|
|
684
773
|
async initializePlayer() {
|
|
685
774
|
if (!this.embedToken) {
|
|
@@ -719,8 +808,12 @@ class GuardVideoPlayer {
|
|
|
719
808
|
enableWorker: true,
|
|
720
809
|
lowLatencyMode: false,
|
|
721
810
|
liveSyncDurationCount: 3,
|
|
722
|
-
manifestLoadingMaxRetry:
|
|
723
|
-
|
|
811
|
+
manifestLoadingMaxRetry: 3,
|
|
812
|
+
manifestLoadingRetryDelay: 2000,
|
|
813
|
+
levelLoadingMaxRetry: 3,
|
|
814
|
+
levelLoadingRetryDelay: 2000,
|
|
815
|
+
fragLoadingMaxRetry: 4,
|
|
816
|
+
fragLoadingRetryDelay: 1000,
|
|
724
817
|
xhrSetup(xhr, url) {
|
|
725
818
|
if (url.includes('/watermark-stream/') && url.includes('.m3u8')) {
|
|
726
819
|
if (self.playlistNonce) {
|
|
@@ -728,10 +821,6 @@ class GuardVideoPlayer {
|
|
|
728
821
|
const nonceUrl = url + separator + 'nonce=' + encodeURIComponent(self.playlistNonce);
|
|
729
822
|
xhr.open('GET', nonceUrl, true);
|
|
730
823
|
self.log('Injected nonce into playlist request');
|
|
731
|
-
self.playlistNonce = null;
|
|
732
|
-
self.refreshNonce().catch(() => {
|
|
733
|
-
self.log('Background nonce refresh failed');
|
|
734
|
-
});
|
|
735
824
|
}
|
|
736
825
|
}
|
|
737
826
|
},
|
|
@@ -743,6 +832,7 @@ class GuardVideoPlayer {
|
|
|
743
832
|
this.log('HLS manifest parsed', { levels: data.levels.map((l) => l.height + 'p') });
|
|
744
833
|
this.setState(PlayerState.READY);
|
|
745
834
|
this.config.onReady();
|
|
835
|
+
this.retryBackoff = 1000;
|
|
746
836
|
if (this.config.autoplay)
|
|
747
837
|
this.play();
|
|
748
838
|
});
|
|
@@ -758,18 +848,52 @@ class GuardVideoPlayer {
|
|
|
758
848
|
this.currentQuality = quality;
|
|
759
849
|
this.log('Quality switched to ' + quality.name);
|
|
760
850
|
this.config.onQualityChange(quality.name);
|
|
851
|
+
this.eventTracker?.track('quality_change', this.videoElement.currentTime, {
|
|
852
|
+
level: data.level,
|
|
853
|
+
height: level.height,
|
|
854
|
+
bitrate: level.bitrate,
|
|
855
|
+
});
|
|
761
856
|
});
|
|
762
857
|
this.hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
763
858
|
this.error('HLS Error', data);
|
|
859
|
+
this.eventTracker?.track('error', this.videoElement.currentTime, {
|
|
860
|
+
type: data.type,
|
|
861
|
+
details: data.details,
|
|
862
|
+
fatal: data.fatal,
|
|
863
|
+
});
|
|
864
|
+
if (Date.now() < this.rateLimitCooldownUntil) {
|
|
865
|
+
this.log('Suppressing retry — rate limit cooldown active (' +
|
|
866
|
+
Math.ceil((this.rateLimitCooldownUntil - Date.now()) / 1000) + 's remaining)');
|
|
867
|
+
if (data.fatal) {
|
|
868
|
+
const delay = this.rateLimitCooldownUntil - Date.now() + 500;
|
|
869
|
+
setTimeout(() => this.hls?.startLoad(), delay);
|
|
870
|
+
}
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const httpStatus = data.response?.code;
|
|
874
|
+
if (httpStatus === 429) {
|
|
875
|
+
this.enterRateLimitCooldown();
|
|
876
|
+
this.log('429 detected — entering cooldown for ' + this.retryBackoff + 'ms');
|
|
877
|
+
setTimeout(() => {
|
|
878
|
+
if (this.embedToken?.forensicWatermark) {
|
|
879
|
+
this.refreshNonce().then(() => this.hls?.startLoad()).catch(() => this.hls?.startLoad());
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
this.hls?.startLoad();
|
|
883
|
+
}
|
|
884
|
+
}, this.retryBackoff);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
764
887
|
if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
|
|
765
888
|
data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
|
|
766
889
|
this.embedToken?.forensicWatermark) {
|
|
767
890
|
this.log('Playlist load failed — refreshing nonce before retry');
|
|
768
891
|
this.refreshNonce().then(() => {
|
|
769
|
-
setTimeout(() => this.hls?.startLoad(),
|
|
892
|
+
setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
|
|
770
893
|
}).catch(() => {
|
|
771
|
-
setTimeout(() => this.hls?.startLoad(),
|
|
894
|
+
setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
|
|
772
895
|
});
|
|
896
|
+
this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
|
|
773
897
|
return;
|
|
774
898
|
}
|
|
775
899
|
if (data.fatal) {
|
|
@@ -779,7 +903,8 @@ class GuardVideoPlayer {
|
|
|
779
903
|
if (this.embedToken?.forensicWatermark) {
|
|
780
904
|
this.refreshNonce().catch(() => { });
|
|
781
905
|
}
|
|
782
|
-
setTimeout(() => this.hls?.startLoad(),
|
|
906
|
+
setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
|
|
907
|
+
this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
|
|
783
908
|
break;
|
|
784
909
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
785
910
|
this.error('Media error, attempting recovery...');
|
|
@@ -857,6 +982,10 @@ class GuardVideoPlayer {
|
|
|
857
982
|
getState() { return this.state; }
|
|
858
983
|
destroy() {
|
|
859
984
|
this.log('Destroying player');
|
|
985
|
+
if (this.nonceRefreshTimer) {
|
|
986
|
+
clearTimeout(this.nonceRefreshTimer);
|
|
987
|
+
this.nonceRefreshTimer = null;
|
|
988
|
+
}
|
|
860
989
|
if (this.eventTracker) {
|
|
861
990
|
this.eventTracker.destroy();
|
|
862
991
|
this.eventTracker = null;
|