@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
|
@@ -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,9 +37287,18 @@ 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;
|
|
@@ -37263,6 +37324,10 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37263
37324
|
async acquireNonce(tokenId) {
|
|
37264
37325
|
if (!this.config.apiBaseUrl)
|
|
37265
37326
|
return null;
|
|
37327
|
+
if (Date.now() < this.rateLimitCooldownUntil) {
|
|
37328
|
+
this.log('Nonce acquisition skipped — rate limit cooldown active');
|
|
37329
|
+
return this.playlistNonce;
|
|
37330
|
+
}
|
|
37266
37331
|
try {
|
|
37267
37332
|
const challengeResult = await this.solveChallenge(tokenId);
|
|
37268
37333
|
const url = this.config.apiBaseUrl + '/videos/playlist-session';
|
|
@@ -37276,12 +37341,19 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37276
37341
|
body: JSON.stringify(body),
|
|
37277
37342
|
credentials: 'omit',
|
|
37278
37343
|
});
|
|
37344
|
+
if (resp.status === 429) {
|
|
37345
|
+
this.enterRateLimitCooldown();
|
|
37346
|
+
this.log('Nonce acquisition hit 429 — entering cooldown');
|
|
37347
|
+
return this.playlistNonce;
|
|
37348
|
+
}
|
|
37279
37349
|
if (!resp.ok) {
|
|
37280
37350
|
this.log('Playlist session nonce acquisition failed', resp.status);
|
|
37281
37351
|
return null;
|
|
37282
37352
|
}
|
|
37283
37353
|
const data = await resp.json();
|
|
37284
37354
|
this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
|
|
37355
|
+
this.retryBackoff = 1000;
|
|
37356
|
+
this.scheduleNonceRefresh(data.expiresIn);
|
|
37285
37357
|
return data.nonce;
|
|
37286
37358
|
}
|
|
37287
37359
|
catch (err) {
|
|
@@ -37289,19 +37361,38 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37289
37361
|
return null;
|
|
37290
37362
|
}
|
|
37291
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
|
+
}
|
|
37292
37374
|
async refreshNonce() {
|
|
37293
|
-
if (this.nonceRefreshInProgress)
|
|
37294
|
-
return;
|
|
37375
|
+
if (this.nonceRefreshInProgress && this.nonceRefreshPromise) {
|
|
37376
|
+
return this.nonceRefreshPromise;
|
|
37377
|
+
}
|
|
37295
37378
|
if (!this.embedToken)
|
|
37296
37379
|
return;
|
|
37297
37380
|
this.nonceRefreshInProgress = true;
|
|
37298
|
-
|
|
37299
|
-
|
|
37300
|
-
|
|
37301
|
-
|
|
37302
|
-
|
|
37303
|
-
|
|
37304
|
-
|
|
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);
|
|
37305
37396
|
}
|
|
37306
37397
|
async initializePlayer() {
|
|
37307
37398
|
if (!this.embedToken) {
|
|
@@ -37341,8 +37432,12 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37341
37432
|
enableWorker: true,
|
|
37342
37433
|
lowLatencyMode: false,
|
|
37343
37434
|
liveSyncDurationCount: 3,
|
|
37344
|
-
manifestLoadingMaxRetry:
|
|
37345
|
-
|
|
37435
|
+
manifestLoadingMaxRetry: 3,
|
|
37436
|
+
manifestLoadingRetryDelay: 2000,
|
|
37437
|
+
levelLoadingMaxRetry: 3,
|
|
37438
|
+
levelLoadingRetryDelay: 2000,
|
|
37439
|
+
fragLoadingMaxRetry: 4,
|
|
37440
|
+
fragLoadingRetryDelay: 1000,
|
|
37346
37441
|
xhrSetup(xhr, url) {
|
|
37347
37442
|
if (url.includes('/watermark-stream/') && url.includes('.m3u8')) {
|
|
37348
37443
|
if (self.playlistNonce) {
|
|
@@ -37350,10 +37445,6 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37350
37445
|
const nonceUrl = url + separator + 'nonce=' + encodeURIComponent(self.playlistNonce);
|
|
37351
37446
|
xhr.open('GET', nonceUrl, true);
|
|
37352
37447
|
self.log('Injected nonce into playlist request');
|
|
37353
|
-
self.playlistNonce = null;
|
|
37354
|
-
self.refreshNonce().catch(() => {
|
|
37355
|
-
self.log('Background nonce refresh failed');
|
|
37356
|
-
});
|
|
37357
37448
|
}
|
|
37358
37449
|
}
|
|
37359
37450
|
},
|
|
@@ -37365,6 +37456,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37365
37456
|
this.log('HLS manifest parsed', { levels: data.levels.map((l) => l.height + 'p') });
|
|
37366
37457
|
this.setState(exports.PlayerState.READY);
|
|
37367
37458
|
this.config.onReady();
|
|
37459
|
+
this.retryBackoff = 1000;
|
|
37368
37460
|
if (this.config.autoplay)
|
|
37369
37461
|
this.play();
|
|
37370
37462
|
});
|
|
@@ -37380,18 +37472,52 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37380
37472
|
this.currentQuality = quality;
|
|
37381
37473
|
this.log('Quality switched to ' + quality.name);
|
|
37382
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
|
+
});
|
|
37383
37480
|
});
|
|
37384
37481
|
this.hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
37385
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
|
+
}
|
|
37386
37511
|
if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
|
|
37387
37512
|
data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
|
|
37388
37513
|
this.embedToken?.forensicWatermark) {
|
|
37389
37514
|
this.log('Playlist load failed — refreshing nonce before retry');
|
|
37390
37515
|
this.refreshNonce().then(() => {
|
|
37391
|
-
setTimeout(() => this.hls?.startLoad(),
|
|
37516
|
+
setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
|
|
37392
37517
|
}).catch(() => {
|
|
37393
|
-
setTimeout(() => this.hls?.startLoad(),
|
|
37518
|
+
setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
|
|
37394
37519
|
});
|
|
37520
|
+
this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
|
|
37395
37521
|
return;
|
|
37396
37522
|
}
|
|
37397
37523
|
if (data.fatal) {
|
|
@@ -37401,7 +37527,8 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37401
37527
|
if (this.embedToken?.forensicWatermark) {
|
|
37402
37528
|
this.refreshNonce().catch(() => { });
|
|
37403
37529
|
}
|
|
37404
|
-
setTimeout(() => this.hls?.startLoad(),
|
|
37530
|
+
setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
|
|
37531
|
+
this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
|
|
37405
37532
|
break;
|
|
37406
37533
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
37407
37534
|
this.error('Media error, attempting recovery...');
|
|
@@ -37479,6 +37606,10 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
|
|
|
37479
37606
|
getState() { return this.state; }
|
|
37480
37607
|
destroy() {
|
|
37481
37608
|
this.log('Destroying player');
|
|
37609
|
+
if (this.nonceRefreshTimer) {
|
|
37610
|
+
clearTimeout(this.nonceRefreshTimer);
|
|
37611
|
+
this.nonceRefreshTimer = null;
|
|
37612
|
+
}
|
|
37482
37613
|
if (this.eventTracker) {
|
|
37483
37614
|
this.eventTracker.destroy();
|
|
37484
37615
|
this.eventTracker = null;
|