@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.
@@ -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
- const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
36751
- await this.sendBatch(batch);
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 && this.config.debug) {
36812
- console.warn('[GuardVideo EventTracker] Flush failed:', resp.status);
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
- try {
37299
- const nonce = await this.acquireNonce(this.embedToken.tokenId);
37300
- this.playlistNonce = nonce;
37301
- }
37302
- finally {
37303
- this.nonceRefreshInProgress = false;
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: 6,
37345
- levelLoadingMaxRetry: 6,
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(), 500);
37516
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
37392
37517
  }).catch(() => {
37393
- setTimeout(() => this.hls?.startLoad(), 1000);
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(), 1000);
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;