@guardvideo/player-sdk 3.5.0 → 3.7.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.
@@ -37077,6 +37077,10 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37077
37077
  this.MAX_BACKOFF = 30000;
37078
37078
  this.rateLimitCooldownUntil = 0;
37079
37079
  this.nonceRefreshPromise = null;
37080
+ this.destroyed = false;
37081
+ this.networkRetryCount = 0;
37082
+ this.MAX_NETWORK_RETRIES = 6;
37083
+ this.pendingRetryTimer = null;
37080
37084
  this._onRateChange = this.enforceMaxRate.bind(this);
37081
37085
  this.videoElement = videoElement;
37082
37086
  this.config = {
@@ -37353,6 +37357,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37353
37357
  const data = await resp.json();
37354
37358
  this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
37355
37359
  this.retryBackoff = 1000;
37360
+ this.networkRetryCount = 0;
37356
37361
  this.scheduleNonceRefresh(data.expiresIn);
37357
37362
  return data.nonce;
37358
37363
  }
@@ -37366,6 +37371,8 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37366
37371
  clearTimeout(this.nonceRefreshTimer);
37367
37372
  const refreshMs = Math.max(5000, expiresInSeconds * 750);
37368
37373
  this.nonceRefreshTimer = setTimeout(() => {
37374
+ if (this.destroyed)
37375
+ return;
37369
37376
  this.refreshNonce().catch(() => {
37370
37377
  this.log('Proactive nonce refresh failed');
37371
37378
  });
@@ -37394,6 +37401,24 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37394
37401
  this.rateLimitCooldownUntil = Date.now() + this.retryBackoff;
37395
37402
  this.retryBackoff = Math.min(this.retryBackoff * 2, this.MAX_BACKOFF);
37396
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;
37421
+ }
37397
37422
  async initializePlayer() {
37398
37423
  if (!this.embedToken) {
37399
37424
  throw new Error('No embed token available');
@@ -37457,6 +37482,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37457
37482
  this.setState(exports.PlayerState.READY);
37458
37483
  this.config.onReady();
37459
37484
  this.retryBackoff = 1000;
37485
+ this.networkRetryCount = 0;
37460
37486
  if (this.config.autoplay)
37461
37487
  this.play();
37462
37488
  });
@@ -37485,12 +37511,14 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37485
37511
  details: data.details,
37486
37512
  fatal: data.fatal,
37487
37513
  });
37514
+ if (this.destroyed)
37515
+ return;
37488
37516
  if (Date.now() < this.rateLimitCooldownUntil) {
37489
37517
  this.log('Suppressing retry — rate limit cooldown active (' +
37490
37518
  Math.ceil((this.rateLimitCooldownUntil - Date.now()) / 1000) + 's remaining)');
37491
37519
  if (data.fatal) {
37492
37520
  const delay = this.rateLimitCooldownUntil - Date.now() + 500;
37493
- setTimeout(() => this.hls?.startLoad(), delay);
37521
+ this.scheduleRetry(() => this.hls?.startLoad(), delay);
37494
37522
  }
37495
37523
  return;
37496
37524
  }
@@ -37498,7 +37526,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37498
37526
  if (httpStatus === 429) {
37499
37527
  this.enterRateLimitCooldown();
37500
37528
  this.log('429 detected — entering cooldown for ' + this.retryBackoff + 'ms');
37501
- setTimeout(() => {
37529
+ this.scheduleRetry(() => {
37502
37530
  if (this.embedToken?.forensicWatermark) {
37503
37531
  this.refreshNonce().then(() => this.hls?.startLoad()).catch(() => this.hls?.startLoad());
37504
37532
  }
@@ -37511,11 +37539,17 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37511
37539
  if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
37512
37540
  data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
37513
37541
  this.embedToken?.forensicWatermark) {
37514
- 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 + ')');
37515
37549
  this.refreshNonce().then(() => {
37516
- setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
37550
+ this.scheduleRetry(() => this.hls?.startLoad(), this.retryBackoff);
37517
37551
  }).catch(() => {
37518
- setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
37552
+ this.scheduleRetry(() => this.hls?.startLoad(), this.retryBackoff);
37519
37553
  });
37520
37554
  this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
37521
37555
  return;
@@ -37523,11 +37557,17 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37523
37557
  if (data.fatal) {
37524
37558
  switch (data.type) {
37525
37559
  case Hls.ErrorTypes.NETWORK_ERROR:
37526
- 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 + ')...');
37527
37567
  if (this.embedToken?.forensicWatermark) {
37528
37568
  this.refreshNonce().catch(() => { });
37529
37569
  }
37530
- setTimeout(() => this.hls?.startLoad(), this.retryBackoff);
37570
+ this.scheduleRetry(() => this.hls?.startLoad(), this.retryBackoff);
37531
37571
  this.retryBackoff = Math.min(this.retryBackoff * 1.5, this.MAX_BACKOFF);
37532
37572
  break;
37533
37573
  case Hls.ErrorTypes.MEDIA_ERROR:
@@ -37542,8 +37582,21 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37542
37582
  this.setupVideoEventListeners();
37543
37583
  }
37544
37584
  setupVideoEventListeners() {
37545
- this.videoElement.addEventListener('playing', () => this.setState(exports.PlayerState.PLAYING));
37546
- 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
+ });
37547
37600
  this.videoElement.addEventListener('waiting', () => this.setState(exports.PlayerState.BUFFERING));
37548
37601
  this.videoElement.addEventListener('error', () => {
37549
37602
  const error = this.videoElement.error;
@@ -37570,6 +37623,8 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37570
37623
  }
37571
37624
  async play() {
37572
37625
  try {
37626
+ this.networkRetryCount = 0;
37627
+ this.retryBackoff = 1000;
37573
37628
  await this.videoElement.play();
37574
37629
  }
37575
37630
  catch (err) {
@@ -37606,6 +37661,11 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37606
37661
  getState() { return this.state; }
37607
37662
  destroy() {
37608
37663
  this.log('Destroying player');
37664
+ this.destroyed = true;
37665
+ if (this.pendingRetryTimer) {
37666
+ clearTimeout(this.pendingRetryTimer);
37667
+ this.pendingRetryTimer = null;
37668
+ }
37609
37669
  if (this.nonceRefreshTimer) {
37610
37670
  clearTimeout(this.nonceRefreshTimer);
37611
37671
  this.nonceRefreshTimer = null;
@@ -38439,6 +38499,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38439
38499
  this._ctxTargetBound = () => { };
38440
38500
  this._ctxKeyDownBound = () => { };
38441
38501
  this._watermarkObserver = null;
38502
+ this._watermarkStylePollTimer = null;
38442
38503
  this._watermarkText = '';
38443
38504
  this._watermarkDriftTimer = null;
38444
38505
  const accent = config.branding?.accentColor ?? '#00e5a0';
@@ -39116,6 +39177,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
39116
39177
  tampered = true;
39117
39178
  }
39118
39179
  if (m.type === 'attributes' && m.target === this.watermarkCanvas) {
39180
+ this.watermarkCanvas.removeAttribute('style');
39119
39181
  this.watermarkCanvas.className = 'gvp-watermark-canvas';
39120
39182
  this.watermarkCanvas.setAttribute('aria-hidden', 'true');
39121
39183
  this._renderCanvasWatermark(this._watermarkText);
@@ -39129,14 +39191,51 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
39129
39191
  }
39130
39192
  }
39131
39193
  if (tampered) {
39132
- this.root.dispatchEvent(new CustomEvent('gv:security', {
39133
- bubbles: true,
39134
- detail: { type: 'watermark_tamper', timestamp: Date.now() },
39135
- }));
39194
+ this._onWatermarkTamper();
39136
39195
  }
39137
39196
  });
39138
39197
  this._watermarkObserver.observe(this.root, { childList: true });
39139
39198
  this._watermarkObserver.observe(this.watermarkDiv, { attributes: true, childList: true });
39199
+ this._watermarkObserver.observe(this.watermarkCanvas, { attributes: true });
39200
+ if (this._watermarkStylePollTimer)
39201
+ clearInterval(this._watermarkStylePollTimer);
39202
+ this._watermarkStylePollTimer = setInterval(() => {
39203
+ if (!this._watermarkText)
39204
+ return;
39205
+ const divHidden = this._isElementHidden(this.watermarkDiv);
39206
+ const canvasHidden = this._isElementHidden(this.watermarkCanvas);
39207
+ if (divHidden || canvasHidden) {
39208
+ this.watermarkDiv.removeAttribute('style');
39209
+ this.watermarkDiv.className = 'gvp-watermark';
39210
+ this.watermarkCanvas.removeAttribute('style');
39211
+ this.watermarkCanvas.className = 'gvp-watermark-canvas';
39212
+ this._purgeWatermarkOverrideStyles();
39213
+ this._onWatermarkTamper();
39214
+ }
39215
+ }, 500);
39216
+ }
39217
+ _isElementHidden(el) {
39218
+ const cs = getComputedStyle(el);
39219
+ return (cs.display === 'none' ||
39220
+ cs.visibility === 'hidden' ||
39221
+ parseFloat(cs.opacity) < 0.01 ||
39222
+ (el.offsetWidth === 0 && el.offsetHeight === 0));
39223
+ }
39224
+ _purgeWatermarkOverrideStyles() {
39225
+ const styles = this.root.querySelectorAll('style');
39226
+ styles.forEach((s) => {
39227
+ const text = s.textContent ?? '';
39228
+ if (/gvp-watermark/i.test(text)) {
39229
+ s.remove();
39230
+ }
39231
+ });
39232
+ }
39233
+ _onWatermarkTamper() {
39234
+ this.corePlayer.pause();
39235
+ this.root.dispatchEvent(new CustomEvent('gv:security', {
39236
+ bubbles: true,
39237
+ detail: { type: 'watermark_tamper', timestamp: Date.now() },
39238
+ }));
39140
39239
  }
39141
39240
  _refillWatermark() {
39142
39241
  const seed = this._hashCode(this._watermarkText);
@@ -39320,6 +39419,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
39320
39419
  }
39321
39420
  play() { return this.corePlayer.play(); }
39322
39421
  pause() { return this.corePlayer.pause(); }
39422
+ stopLoading() { return this.corePlayer.stopLoading(); }
39323
39423
  seek(t) { return this.corePlayer.seek(t); }
39324
39424
  getCurrentTime() { return this.corePlayer.getCurrentTime(); }
39325
39425
  getDuration() { return this.corePlayer.getDuration(); }
@@ -39347,6 +39447,8 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
39347
39447
  this._watermarkObserver?.disconnect();
39348
39448
  if (this._watermarkDriftTimer)
39349
39449
  clearInterval(this._watermarkDriftTimer);
39450
+ if (this._watermarkStylePollTimer)
39451
+ clearInterval(this._watermarkStylePollTimer);
39350
39452
  this.corePlayer.destroy();
39351
39453
  this.root.remove();
39352
39454
  }