@guardvideo/player-sdk 3.4.0 → 3.6.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,15 @@ 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;
37080
+ this.destroyed = false;
37081
+ this.networkRetryCount = 0;
37082
+ this.MAX_NETWORK_RETRIES = 6;
37083
+ this.pendingRetryTimer = null;
37028
37084
  this._onRateChange = this.enforceMaxRate.bind(this);
37029
37085
  this.videoElement = videoElement;
37030
37086
  this.config = {
@@ -37235,9 +37291,18 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37235
37291
  async solveChallenge(tokenId) {
37236
37292
  if (!this.config.apiBaseUrl)
37237
37293
  return undefined;
37294
+ if (Date.now() < this.rateLimitCooldownUntil) {
37295
+ this.log('Challenge skipped — rate limit cooldown active');
37296
+ return undefined;
37297
+ }
37238
37298
  try {
37239
37299
  const url = this.config.apiBaseUrl + '/videos/challenge/' + encodeURIComponent(tokenId);
37240
37300
  const resp = await fetch(url, { credentials: 'omit' });
37301
+ if (resp.status === 429) {
37302
+ this.enterRateLimitCooldown();
37303
+ this.log('Challenge hit 429 — entering cooldown');
37304
+ return undefined;
37305
+ }
37241
37306
  if (!resp.ok) {
37242
37307
  this.log('Challenge fetch failed (challenge may be disabled)', resp.status);
37243
37308
  return undefined;
@@ -37263,6 +37328,10 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37263
37328
  async acquireNonce(tokenId) {
37264
37329
  if (!this.config.apiBaseUrl)
37265
37330
  return null;
37331
+ if (Date.now() < this.rateLimitCooldownUntil) {
37332
+ this.log('Nonce acquisition skipped — rate limit cooldown active');
37333
+ return this.playlistNonce;
37334
+ }
37266
37335
  try {
37267
37336
  const challengeResult = await this.solveChallenge(tokenId);
37268
37337
  const url = this.config.apiBaseUrl + '/videos/playlist-session';
@@ -37276,12 +37345,20 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37276
37345
  body: JSON.stringify(body),
37277
37346
  credentials: 'omit',
37278
37347
  });
37348
+ if (resp.status === 429) {
37349
+ this.enterRateLimitCooldown();
37350
+ this.log('Nonce acquisition hit 429 — entering cooldown');
37351
+ return this.playlistNonce;
37352
+ }
37279
37353
  if (!resp.ok) {
37280
37354
  this.log('Playlist session nonce acquisition failed', resp.status);
37281
37355
  return null;
37282
37356
  }
37283
37357
  const data = await resp.json();
37284
37358
  this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
37359
+ this.retryBackoff = 1000;
37360
+ this.networkRetryCount = 0;
37361
+ this.scheduleNonceRefresh(data.expiresIn);
37285
37362
  return data.nonce;
37286
37363
  }
37287
37364
  catch (err) {
@@ -37289,19 +37366,58 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37289
37366
  return null;
37290
37367
  }
37291
37368
  }
37369
+ scheduleNonceRefresh(expiresInSeconds) {
37370
+ if (this.nonceRefreshTimer)
37371
+ clearTimeout(this.nonceRefreshTimer);
37372
+ const refreshMs = Math.max(5000, expiresInSeconds * 750);
37373
+ this.nonceRefreshTimer = setTimeout(() => {
37374
+ if (this.destroyed)
37375
+ return;
37376
+ this.refreshNonce().catch(() => {
37377
+ this.log('Proactive nonce refresh failed');
37378
+ });
37379
+ }, refreshMs);
37380
+ }
37292
37381
  async refreshNonce() {
37293
- if (this.nonceRefreshInProgress)
37294
- return;
37382
+ if (this.nonceRefreshInProgress && this.nonceRefreshPromise) {
37383
+ return this.nonceRefreshPromise;
37384
+ }
37295
37385
  if (!this.embedToken)
37296
37386
  return;
37297
37387
  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
- }
37388
+ this.nonceRefreshPromise = (async () => {
37389
+ try {
37390
+ const nonce = await this.acquireNonce(this.embedToken.tokenId);
37391
+ this.playlistNonce = nonce;
37392
+ }
37393
+ finally {
37394
+ this.nonceRefreshInProgress = false;
37395
+ this.nonceRefreshPromise = null;
37396
+ }
37397
+ })();
37398
+ return this.nonceRefreshPromise;
37399
+ }
37400
+ enterRateLimitCooldown() {
37401
+ this.rateLimitCooldownUntil = Date.now() + this.retryBackoff;
37402
+ this.retryBackoff = Math.min(this.retryBackoff * 2, this.MAX_BACKOFF);
37403
+ }
37404
+ scheduleRetry(fn, delayMs) {
37405
+ if (this.pendingRetryTimer)
37406
+ clearTimeout(this.pendingRetryTimer);
37407
+ this.pendingRetryTimer = setTimeout(() => {
37408
+ this.pendingRetryTimer = null;
37409
+ if (!this.destroyed && this.hls)
37410
+ fn();
37411
+ }, delayMs);
37412
+ }
37413
+ stopLoading() {
37414
+ if (this.pendingRetryTimer) {
37415
+ clearTimeout(this.pendingRetryTimer);
37416
+ this.pendingRetryTimer = null;
37417
+ }
37418
+ this.hls?.stopLoad();
37419
+ this.networkRetryCount = 0;
37420
+ this.retryBackoff = 1000;
37305
37421
  }
37306
37422
  async initializePlayer() {
37307
37423
  if (!this.embedToken) {
@@ -37341,8 +37457,12 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37341
37457
  enableWorker: true,
37342
37458
  lowLatencyMode: false,
37343
37459
  liveSyncDurationCount: 3,
37344
- manifestLoadingMaxRetry: 6,
37345
- levelLoadingMaxRetry: 6,
37460
+ manifestLoadingMaxRetry: 3,
37461
+ manifestLoadingRetryDelay: 2000,
37462
+ levelLoadingMaxRetry: 3,
37463
+ levelLoadingRetryDelay: 2000,
37464
+ fragLoadingMaxRetry: 4,
37465
+ fragLoadingRetryDelay: 1000,
37346
37466
  xhrSetup(xhr, url) {
37347
37467
  if (url.includes('/watermark-stream/') && url.includes('.m3u8')) {
37348
37468
  if (self.playlistNonce) {
@@ -37350,10 +37470,6 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37350
37470
  const nonceUrl = url + separator + 'nonce=' + encodeURIComponent(self.playlistNonce);
37351
37471
  xhr.open('GET', nonceUrl, true);
37352
37472
  self.log('Injected nonce into playlist request');
37353
- self.playlistNonce = null;
37354
- self.refreshNonce().catch(() => {
37355
- self.log('Background nonce refresh failed');
37356
- });
37357
37473
  }
37358
37474
  }
37359
37475
  },
@@ -37365,6 +37481,8 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37365
37481
  this.log('HLS manifest parsed', { levels: data.levels.map((l) => l.height + 'p') });
37366
37482
  this.setState(exports.PlayerState.READY);
37367
37483
  this.config.onReady();
37484
+ this.retryBackoff = 1000;
37485
+ this.networkRetryCount = 0;
37368
37486
  if (this.config.autoplay)
37369
37487
  this.play();
37370
37488
  });
@@ -37380,28 +37498,77 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37380
37498
  this.currentQuality = quality;
37381
37499
  this.log('Quality switched to ' + quality.name);
37382
37500
  this.config.onQualityChange(quality.name);
37501
+ this.eventTracker?.track('quality_change', this.videoElement.currentTime, {
37502
+ level: data.level,
37503
+ height: level.height,
37504
+ bitrate: level.bitrate,
37505
+ });
37383
37506
  });
37384
37507
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
37385
37508
  this.error('HLS Error', data);
37509
+ this.eventTracker?.track('error', this.videoElement.currentTime, {
37510
+ type: data.type,
37511
+ details: data.details,
37512
+ fatal: data.fatal,
37513
+ });
37514
+ if (this.destroyed)
37515
+ return;
37516
+ if (Date.now() < this.rateLimitCooldownUntil) {
37517
+ this.log('Suppressing retry — rate limit cooldown active (' +
37518
+ Math.ceil((this.rateLimitCooldownUntil - Date.now()) / 1000) + 's remaining)');
37519
+ if (data.fatal) {
37520
+ const delay = this.rateLimitCooldownUntil - Date.now() + 500;
37521
+ this.scheduleRetry(() => this.hls?.startLoad(), delay);
37522
+ }
37523
+ return;
37524
+ }
37525
+ const httpStatus = data.response?.code;
37526
+ if (httpStatus === 429) {
37527
+ this.enterRateLimitCooldown();
37528
+ this.log('429 detected — entering cooldown for ' + this.retryBackoff + 'ms');
37529
+ this.scheduleRetry(() => {
37530
+ if (this.embedToken?.forensicWatermark) {
37531
+ this.refreshNonce().then(() => this.hls?.startLoad()).catch(() => this.hls?.startLoad());
37532
+ }
37533
+ else {
37534
+ this.hls?.startLoad();
37535
+ }
37536
+ }, this.retryBackoff);
37537
+ return;
37538
+ }
37386
37539
  if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
37387
37540
  data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
37388
37541
  this.embedToken?.forensicWatermark) {
37389
- this.log('Playlist load failed — refreshing nonce before retry');
37542
+ this.networkRetryCount++;
37543
+ if (this.networkRetryCount > this.MAX_NETWORK_RETRIES) {
37544
+ this.error('Max network retries exceeded for playlist load');
37545
+ this.handleError({ code: 'NETWORK_ERROR', message: 'Playlist load failed after multiple retries', fatal: true, details: data });
37546
+ return;
37547
+ }
37548
+ this.log('Playlist load failed — refreshing nonce before retry (' + this.networkRetryCount + '/' + this.MAX_NETWORK_RETRIES + ')');
37390
37549
  this.refreshNonce().then(() => {
37391
- setTimeout(() => this.hls?.startLoad(), 500);
37550
+ this.scheduleRetry(() => this.hls?.startLoad(), this.retryBackoff);
37392
37551
  }).catch(() => {
37393
- setTimeout(() => this.hls?.startLoad(), 1000);
37552
+ this.scheduleRetry(() => this.hls?.startLoad(), this.retryBackoff);
37394
37553
  });
37554
+ this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
37395
37555
  return;
37396
37556
  }
37397
37557
  if (data.fatal) {
37398
37558
  switch (data.type) {
37399
37559
  case Hls.ErrorTypes.NETWORK_ERROR:
37400
- this.error('Network error, attempting recovery...');
37560
+ this.networkRetryCount++;
37561
+ if (this.networkRetryCount > this.MAX_NETWORK_RETRIES) {
37562
+ this.error('Max network retries exceeded — giving up');
37563
+ this.handleError({ code: 'NETWORK_ERROR', message: 'Network error after multiple retries. Please check your connection.', fatal: true, details: data });
37564
+ return;
37565
+ }
37566
+ this.error('Network error, attempting recovery (' + this.networkRetryCount + '/' + this.MAX_NETWORK_RETRIES + ')...');
37401
37567
  if (this.embedToken?.forensicWatermark) {
37402
37568
  this.refreshNonce().catch(() => { });
37403
37569
  }
37404
- setTimeout(() => this.hls?.startLoad(), 1000);
37570
+ this.scheduleRetry(() => this.hls?.startLoad(), this.retryBackoff);
37571
+ this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
37405
37572
  break;
37406
37573
  case Hls.ErrorTypes.MEDIA_ERROR:
37407
37574
  this.error('Media error, attempting recovery...');
@@ -37415,8 +37582,21 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37415
37582
  this.setupVideoEventListeners();
37416
37583
  }
37417
37584
  setupVideoEventListeners() {
37418
- this.videoElement.addEventListener('playing', () => this.setState(exports.PlayerState.PLAYING));
37419
- this.videoElement.addEventListener('pause', () => this.setState(exports.PlayerState.PAUSED));
37585
+ this.videoElement.addEventListener('playing', () => {
37586
+ this.setState(exports.PlayerState.PLAYING);
37587
+ if (this.hls)
37588
+ this.hls.startLoad(-1);
37589
+ });
37590
+ this.videoElement.addEventListener('pause', () => {
37591
+ this.setState(exports.PlayerState.PAUSED);
37592
+ if (this.hls && !this.videoElement.seeking) {
37593
+ this.hls.stopLoad();
37594
+ if (this.pendingRetryTimer) {
37595
+ clearTimeout(this.pendingRetryTimer);
37596
+ this.pendingRetryTimer = null;
37597
+ }
37598
+ }
37599
+ });
37420
37600
  this.videoElement.addEventListener('waiting', () => this.setState(exports.PlayerState.BUFFERING));
37421
37601
  this.videoElement.addEventListener('error', () => {
37422
37602
  const error = this.videoElement.error;
@@ -37443,6 +37623,8 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37443
37623
  }
37444
37624
  async play() {
37445
37625
  try {
37626
+ this.networkRetryCount = 0;
37627
+ this.retryBackoff = 1000;
37446
37628
  await this.videoElement.play();
37447
37629
  }
37448
37630
  catch (err) {
@@ -37479,6 +37661,15 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37479
37661
  getState() { return this.state; }
37480
37662
  destroy() {
37481
37663
  this.log('Destroying player');
37664
+ this.destroyed = true;
37665
+ if (this.pendingRetryTimer) {
37666
+ clearTimeout(this.pendingRetryTimer);
37667
+ this.pendingRetryTimer = null;
37668
+ }
37669
+ if (this.nonceRefreshTimer) {
37670
+ clearTimeout(this.nonceRefreshTimer);
37671
+ this.nonceRefreshTimer = null;
37672
+ }
37482
37673
  if (this.eventTracker) {
37483
37674
  this.eventTracker.destroy();
37484
37675
  this.eventTracker = null;
@@ -39189,6 +39380,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
39189
39380
  }
39190
39381
  play() { return this.corePlayer.play(); }
39191
39382
  pause() { return this.corePlayer.pause(); }
39383
+ stopLoading() { return this.corePlayer.stopLoading(); }
39192
39384
  seek(t) { return this.corePlayer.seek(t); }
39193
39385
  getCurrentTime() { return this.corePlayer.getCurrentTime(); }
39194
39386
  getDuration() { return this.corePlayer.getDuration(); }