@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.
@@ -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,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 response...');
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
- const derivedBits = await crypto.subtle.deriveBits({
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
- const hashArray = Array.from(new Uint8Array(derivedBits));
37256
- const hexResponse = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
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 challengeResponse = await this.solveChallenge(tokenId);
37332
+ const challengeResult = await this.solveChallenge(tokenId);
37270
37333
  const url = this.config.apiBaseUrl + '/videos/playlist-session';
37271
37334
  const body = { tokenId };
37272
- if (challengeResponse) {
37273
- body.challengeResponse = challengeResponse;
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
- try {
37301
- const nonce = await this.acquireNonce(this.embedToken.tokenId);
37302
- this.playlistNonce = nonce;
37303
- }
37304
- finally {
37305
- this.nonceRefreshInProgress = false;
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: 6,
37347
- levelLoadingMaxRetry: 6,
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(), 500);
37516
+ setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
37394
37517
  }).catch(() => {
37395
- setTimeout(() => this.hls?.startLoad(), 1000);
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(), 1000);
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;