@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
|
@@ -36692,8 +36692,19 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
36692
36692
|
}
|
|
36693
36693
|
}
|
|
36694
36694
|
|
|
36695
|
+
const VALID_EVENT_TYPES = [
|
|
36696
|
+
'watch_chunk',
|
|
36697
|
+
'play',
|
|
36698
|
+
'pause',
|
|
36699
|
+
'seek',
|
|
36700
|
+
'ended',
|
|
36701
|
+
'quality_change',
|
|
36702
|
+
'error',
|
|
36703
|
+
'security_event',
|
|
36704
|
+
];
|
|
36695
36705
|
const MAX_BATCH_SIZE = 25;
|
|
36696
36706
|
const DEFAULT_FLUSH_INTERVAL = 5000;
|
|
36707
|
+
const MAX_FLUSH_BACKOFF = 60000;
|
|
36697
36708
|
class EventTracker {
|
|
36698
36709
|
constructor(config) {
|
|
36699
36710
|
this.buffer = [];
|
|
@@ -36701,6 +36712,8 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
36701
36712
|
this.accumulator = new WatchChunkAccumulator();
|
|
36702
36713
|
this.destroyed = false;
|
|
36703
36714
|
this._onBeforeUnload = null;
|
|
36715
|
+
this.flushBackoff = 0;
|
|
36716
|
+
this.flushing = false;
|
|
36704
36717
|
this.config = {
|
|
36705
36718
|
endpoint: config.endpoint,
|
|
36706
36719
|
tokenId: config.tokenId,
|
|
@@ -36716,6 +36729,12 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
36716
36729
|
track(type, positionSeconds, payload) {
|
|
36717
36730
|
if (this.destroyed)
|
|
36718
36731
|
return;
|
|
36732
|
+
if (!VALID_EVENT_TYPES.includes(type)) {
|
|
36733
|
+
if (this.config.debug) {
|
|
36734
|
+
console.warn('[GuardVideo EventTracker] Skipping unknown event type:', type);
|
|
36735
|
+
}
|
|
36736
|
+
return;
|
|
36737
|
+
}
|
|
36719
36738
|
this.buffer.push({
|
|
36720
36739
|
event_type: type,
|
|
36721
36740
|
event_at: new Date().toISOString(),
|
|
@@ -36739,7 +36758,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
36739
36758
|
this.config.sessionId = sessionId;
|
|
36740
36759
|
}
|
|
36741
36760
|
async flush() {
|
|
36742
|
-
if (this.destroyed)
|
|
36761
|
+
if (this.destroyed || this.flushing)
|
|
36743
36762
|
return;
|
|
36744
36763
|
const chunks = this.accumulator.drain();
|
|
36745
36764
|
for (const c of chunks) {
|
|
@@ -36747,8 +36766,14 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
36747
36766
|
}
|
|
36748
36767
|
if (this.buffer.length === 0)
|
|
36749
36768
|
return;
|
|
36750
|
-
|
|
36751
|
-
|
|
36769
|
+
this.flushing = true;
|
|
36770
|
+
try {
|
|
36771
|
+
const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
|
|
36772
|
+
await this.sendBatch(batch);
|
|
36773
|
+
}
|
|
36774
|
+
finally {
|
|
36775
|
+
this.flushing = false;
|
|
36776
|
+
}
|
|
36752
36777
|
}
|
|
36753
36778
|
destroy() {
|
|
36754
36779
|
this.destroyed = true;
|
|
@@ -36763,6 +36788,11 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
36763
36788
|
startAutoFlush() {
|
|
36764
36789
|
this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
|
|
36765
36790
|
}
|
|
36791
|
+
restartAutoFlush(intervalMs) {
|
|
36792
|
+
if (this.flushTimer)
|
|
36793
|
+
clearInterval(this.flushTimer);
|
|
36794
|
+
this.flushTimer = setInterval(() => this.flush(), intervalMs);
|
|
36795
|
+
}
|
|
36766
36796
|
hookPageUnload() {
|
|
36767
36797
|
if (typeof window === 'undefined')
|
|
36768
36798
|
return;
|
|
@@ -36808,8 +36838,25 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
36808
36838
|
body,
|
|
36809
36839
|
credentials: 'omit',
|
|
36810
36840
|
});
|
|
36811
|
-
if (!resp.ok
|
|
36812
|
-
|
|
36841
|
+
if (!resp.ok) {
|
|
36842
|
+
if (resp.status === 429) {
|
|
36843
|
+
this.flushBackoff = Math.min((this.flushBackoff || this.config.flushIntervalMs) * 2, MAX_FLUSH_BACKOFF);
|
|
36844
|
+
if (this.config.debug) {
|
|
36845
|
+
console.warn('[GuardVideo EventTracker] 429 — backing off to', this.flushBackoff, 'ms');
|
|
36846
|
+
}
|
|
36847
|
+
this.buffer.unshift(...events);
|
|
36848
|
+
this.restartAutoFlush(this.flushBackoff);
|
|
36849
|
+
return;
|
|
36850
|
+
}
|
|
36851
|
+
if (this.config.debug) {
|
|
36852
|
+
console.warn('[GuardVideo EventTracker] Flush failed:', resp.status);
|
|
36853
|
+
}
|
|
36854
|
+
}
|
|
36855
|
+
else {
|
|
36856
|
+
if (this.flushBackoff > 0) {
|
|
36857
|
+
this.flushBackoff = 0;
|
|
36858
|
+
this.restartAutoFlush(this.config.flushIntervalMs);
|
|
36859
|
+
}
|
|
36813
36860
|
}
|
|
36814
36861
|
}
|
|
36815
36862
|
catch (err) {
|
|
@@ -37025,6 +37072,11 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37025
37072
|
this._onSecurityEvent = null;
|
|
37026
37073
|
this.playlistNonce = null;
|
|
37027
37074
|
this.nonceRefreshInProgress = false;
|
|
37075
|
+
this.nonceRefreshTimer = null;
|
|
37076
|
+
this.retryBackoff = 1000;
|
|
37077
|
+
this.MAX_BACKOFF = 30000;
|
|
37078
|
+
this.rateLimitCooldownUntil = 0;
|
|
37079
|
+
this.nonceRefreshPromise = null;
|
|
37028
37080
|
this._onRateChange = this.enforceMaxRate.bind(this);
|
|
37029
37081
|
this.videoElement = videoElement;
|
|
37030
37082
|
this.config = {
|
|
@@ -37235,27 +37287,34 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37235
37287
|
async solveChallenge(tokenId) {
|
|
37236
37288
|
if (!this.config.apiBaseUrl)
|
|
37237
37289
|
return undefined;
|
|
37290
|
+
if (Date.now() < this.rateLimitCooldownUntil) {
|
|
37291
|
+
this.log('Challenge skipped — rate limit cooldown active');
|
|
37292
|
+
return undefined;
|
|
37293
|
+
}
|
|
37238
37294
|
try {
|
|
37239
37295
|
const url = this.config.apiBaseUrl + '/videos/challenge/' + encodeURIComponent(tokenId);
|
|
37240
37296
|
const resp = await fetch(url, { credentials: 'omit' });
|
|
37297
|
+
if (resp.status === 429) {
|
|
37298
|
+
this.enterRateLimitCooldown();
|
|
37299
|
+
this.log('Challenge hit 429 — entering cooldown');
|
|
37300
|
+
return undefined;
|
|
37301
|
+
}
|
|
37241
37302
|
if (!resp.ok) {
|
|
37242
37303
|
this.log('Challenge fetch failed (challenge may be disabled)', resp.status);
|
|
37243
37304
|
return undefined;
|
|
37244
37305
|
}
|
|
37245
37306
|
const challenge = await resp.json();
|
|
37246
|
-
this.log('Challenge received, computing PBKDF2
|
|
37307
|
+
this.log('Challenge received, computing PBKDF2 proof-of-work...');
|
|
37247
37308
|
const encoder = new TextEncoder();
|
|
37248
37309
|
const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(challenge.nonce), 'PBKDF2', false, ['deriveBits']);
|
|
37249
|
-
|
|
37310
|
+
await crypto.subtle.deriveBits({
|
|
37250
37311
|
name: 'PBKDF2',
|
|
37251
37312
|
salt: encoder.encode(challenge.salt),
|
|
37252
37313
|
iterations: challenge.iterations,
|
|
37253
37314
|
hash: 'SHA-256',
|
|
37254
37315
|
}, keyMaterial, challenge.keyLength * 8);
|
|
37255
|
-
|
|
37256
|
-
|
|
37257
|
-
this.log('Challenge response computed');
|
|
37258
|
-
return hexResponse;
|
|
37316
|
+
this.log('Challenge proof-of-work completed');
|
|
37317
|
+
return { nonce: challenge.nonce };
|
|
37259
37318
|
}
|
|
37260
37319
|
catch (err) {
|
|
37261
37320
|
this.log('Challenge computation failed (non-fatal):', err);
|
|
@@ -37265,12 +37324,16 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37265
37324
|
async acquireNonce(tokenId) {
|
|
37266
37325
|
if (!this.config.apiBaseUrl)
|
|
37267
37326
|
return null;
|
|
37327
|
+
if (Date.now() < this.rateLimitCooldownUntil) {
|
|
37328
|
+
this.log('Nonce acquisition skipped — rate limit cooldown active');
|
|
37329
|
+
return this.playlistNonce;
|
|
37330
|
+
}
|
|
37268
37331
|
try {
|
|
37269
|
-
const
|
|
37332
|
+
const challengeResult = await this.solveChallenge(tokenId);
|
|
37270
37333
|
const url = this.config.apiBaseUrl + '/videos/playlist-session';
|
|
37271
37334
|
const body = { tokenId };
|
|
37272
|
-
if (
|
|
37273
|
-
body.
|
|
37335
|
+
if (challengeResult) {
|
|
37336
|
+
body.challengeNonce = challengeResult.nonce;
|
|
37274
37337
|
}
|
|
37275
37338
|
const resp = await fetch(url, {
|
|
37276
37339
|
method: 'POST',
|
|
@@ -37278,12 +37341,19 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37278
37341
|
body: JSON.stringify(body),
|
|
37279
37342
|
credentials: 'omit',
|
|
37280
37343
|
});
|
|
37344
|
+
if (resp.status === 429) {
|
|
37345
|
+
this.enterRateLimitCooldown();
|
|
37346
|
+
this.log('Nonce acquisition hit 429 — entering cooldown');
|
|
37347
|
+
return this.playlistNonce;
|
|
37348
|
+
}
|
|
37281
37349
|
if (!resp.ok) {
|
|
37282
37350
|
this.log('Playlist session nonce acquisition failed', resp.status);
|
|
37283
37351
|
return null;
|
|
37284
37352
|
}
|
|
37285
37353
|
const data = await resp.json();
|
|
37286
37354
|
this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
|
|
37355
|
+
this.retryBackoff = 1000;
|
|
37356
|
+
this.scheduleNonceRefresh(data.expiresIn);
|
|
37287
37357
|
return data.nonce;
|
|
37288
37358
|
}
|
|
37289
37359
|
catch (err) {
|
|
@@ -37291,19 +37361,38 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37291
37361
|
return null;
|
|
37292
37362
|
}
|
|
37293
37363
|
}
|
|
37364
|
+
scheduleNonceRefresh(expiresInSeconds) {
|
|
37365
|
+
if (this.nonceRefreshTimer)
|
|
37366
|
+
clearTimeout(this.nonceRefreshTimer);
|
|
37367
|
+
const refreshMs = Math.max(5000, expiresInSeconds * 750);
|
|
37368
|
+
this.nonceRefreshTimer = setTimeout(() => {
|
|
37369
|
+
this.refreshNonce().catch(() => {
|
|
37370
|
+
this.log('Proactive nonce refresh failed');
|
|
37371
|
+
});
|
|
37372
|
+
}, refreshMs);
|
|
37373
|
+
}
|
|
37294
37374
|
async refreshNonce() {
|
|
37295
|
-
if (this.nonceRefreshInProgress)
|
|
37296
|
-
return;
|
|
37375
|
+
if (this.nonceRefreshInProgress && this.nonceRefreshPromise) {
|
|
37376
|
+
return this.nonceRefreshPromise;
|
|
37377
|
+
}
|
|
37297
37378
|
if (!this.embedToken)
|
|
37298
37379
|
return;
|
|
37299
37380
|
this.nonceRefreshInProgress = true;
|
|
37300
|
-
|
|
37301
|
-
|
|
37302
|
-
|
|
37303
|
-
|
|
37304
|
-
|
|
37305
|
-
|
|
37306
|
-
|
|
37381
|
+
this.nonceRefreshPromise = (async () => {
|
|
37382
|
+
try {
|
|
37383
|
+
const nonce = await this.acquireNonce(this.embedToken.tokenId);
|
|
37384
|
+
this.playlistNonce = nonce;
|
|
37385
|
+
}
|
|
37386
|
+
finally {
|
|
37387
|
+
this.nonceRefreshInProgress = false;
|
|
37388
|
+
this.nonceRefreshPromise = null;
|
|
37389
|
+
}
|
|
37390
|
+
})();
|
|
37391
|
+
return this.nonceRefreshPromise;
|
|
37392
|
+
}
|
|
37393
|
+
enterRateLimitCooldown() {
|
|
37394
|
+
this.rateLimitCooldownUntil = Date.now() + this.retryBackoff;
|
|
37395
|
+
this.retryBackoff = Math.min(this.retryBackoff * 2, this.MAX_BACKOFF);
|
|
37307
37396
|
}
|
|
37308
37397
|
async initializePlayer() {
|
|
37309
37398
|
if (!this.embedToken) {
|
|
@@ -37343,8 +37432,12 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37343
37432
|
enableWorker: true,
|
|
37344
37433
|
lowLatencyMode: false,
|
|
37345
37434
|
liveSyncDurationCount: 3,
|
|
37346
|
-
manifestLoadingMaxRetry:
|
|
37347
|
-
|
|
37435
|
+
manifestLoadingMaxRetry: 3,
|
|
37436
|
+
manifestLoadingRetryDelay: 2000,
|
|
37437
|
+
levelLoadingMaxRetry: 3,
|
|
37438
|
+
levelLoadingRetryDelay: 2000,
|
|
37439
|
+
fragLoadingMaxRetry: 4,
|
|
37440
|
+
fragLoadingRetryDelay: 1000,
|
|
37348
37441
|
xhrSetup(xhr, url) {
|
|
37349
37442
|
if (url.includes('/watermark-stream/') && url.includes('.m3u8')) {
|
|
37350
37443
|
if (self.playlistNonce) {
|
|
@@ -37352,10 +37445,6 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37352
37445
|
const nonceUrl = url + separator + 'nonce=' + encodeURIComponent(self.playlistNonce);
|
|
37353
37446
|
xhr.open('GET', nonceUrl, true);
|
|
37354
37447
|
self.log('Injected nonce into playlist request');
|
|
37355
|
-
self.playlistNonce = null;
|
|
37356
|
-
self.refreshNonce().catch(() => {
|
|
37357
|
-
self.log('Background nonce refresh failed');
|
|
37358
|
-
});
|
|
37359
37448
|
}
|
|
37360
37449
|
}
|
|
37361
37450
|
},
|
|
@@ -37367,6 +37456,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37367
37456
|
this.log('HLS manifest parsed', { levels: data.levels.map((l) => l.height + 'p') });
|
|
37368
37457
|
this.setState(exports.PlayerState.READY);
|
|
37369
37458
|
this.config.onReady();
|
|
37459
|
+
this.retryBackoff = 1000;
|
|
37370
37460
|
if (this.config.autoplay)
|
|
37371
37461
|
this.play();
|
|
37372
37462
|
});
|
|
@@ -37382,18 +37472,52 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37382
37472
|
this.currentQuality = quality;
|
|
37383
37473
|
this.log('Quality switched to ' + quality.name);
|
|
37384
37474
|
this.config.onQualityChange(quality.name);
|
|
37475
|
+
this.eventTracker?.track('quality_change', this.videoElement.currentTime, {
|
|
37476
|
+
level: data.level,
|
|
37477
|
+
height: level.height,
|
|
37478
|
+
bitrate: level.bitrate,
|
|
37479
|
+
});
|
|
37385
37480
|
});
|
|
37386
37481
|
this.hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
37387
37482
|
this.error('HLS Error', data);
|
|
37483
|
+
this.eventTracker?.track('error', this.videoElement.currentTime, {
|
|
37484
|
+
type: data.type,
|
|
37485
|
+
details: data.details,
|
|
37486
|
+
fatal: data.fatal,
|
|
37487
|
+
});
|
|
37488
|
+
if (Date.now() < this.rateLimitCooldownUntil) {
|
|
37489
|
+
this.log('Suppressing retry — rate limit cooldown active (' +
|
|
37490
|
+
Math.ceil((this.rateLimitCooldownUntil - Date.now()) / 1000) + 's remaining)');
|
|
37491
|
+
if (data.fatal) {
|
|
37492
|
+
const delay = this.rateLimitCooldownUntil - Date.now() + 500;
|
|
37493
|
+
setTimeout(() => this.hls?.startLoad(), delay);
|
|
37494
|
+
}
|
|
37495
|
+
return;
|
|
37496
|
+
}
|
|
37497
|
+
const httpStatus = data.response?.code;
|
|
37498
|
+
if (httpStatus === 429) {
|
|
37499
|
+
this.enterRateLimitCooldown();
|
|
37500
|
+
this.log('429 detected — entering cooldown for ' + this.retryBackoff + 'ms');
|
|
37501
|
+
setTimeout(() => {
|
|
37502
|
+
if (this.embedToken?.forensicWatermark) {
|
|
37503
|
+
this.refreshNonce().then(() => this.hls?.startLoad()).catch(() => this.hls?.startLoad());
|
|
37504
|
+
}
|
|
37505
|
+
else {
|
|
37506
|
+
this.hls?.startLoad();
|
|
37507
|
+
}
|
|
37508
|
+
}, this.retryBackoff);
|
|
37509
|
+
return;
|
|
37510
|
+
}
|
|
37388
37511
|
if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
|
|
37389
37512
|
data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
|
|
37390
37513
|
this.embedToken?.forensicWatermark) {
|
|
37391
37514
|
this.log('Playlist load failed — refreshing nonce before retry');
|
|
37392
37515
|
this.refreshNonce().then(() => {
|
|
37393
|
-
setTimeout(() => this.hls?.startLoad(),
|
|
37516
|
+
setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
|
|
37394
37517
|
}).catch(() => {
|
|
37395
|
-
setTimeout(() => this.hls?.startLoad(),
|
|
37518
|
+
setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
|
|
37396
37519
|
});
|
|
37520
|
+
this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
|
|
37397
37521
|
return;
|
|
37398
37522
|
}
|
|
37399
37523
|
if (data.fatal) {
|
|
@@ -37403,7 +37527,8 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37403
37527
|
if (this.embedToken?.forensicWatermark) {
|
|
37404
37528
|
this.refreshNonce().catch(() => { });
|
|
37405
37529
|
}
|
|
37406
|
-
setTimeout(() => this.hls?.startLoad(),
|
|
37530
|
+
setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
|
|
37531
|
+
this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
|
|
37407
37532
|
break;
|
|
37408
37533
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
37409
37534
|
this.error('Media error, attempting recovery...');
|
|
@@ -37481,6 +37606,10 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37481
37606
|
getState() { return this.state; }
|
|
37482
37607
|
destroy() {
|
|
37483
37608
|
this.log('Destroying player');
|
|
37609
|
+
if (this.nonceRefreshTimer) {
|
|
37610
|
+
clearTimeout(this.nonceRefreshTimer);
|
|
37611
|
+
this.nonceRefreshTimer = null;
|
|
37612
|
+
}
|
|
37484
37613
|
if (this.eventTracker) {
|
|
37485
37614
|
this.eventTracker.destroy();
|
|
37486
37615
|
this.eventTracker = null;
|