@guardvideo/player-sdk 3.2.0 → 3.4.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.
@@ -37023,6 +37023,8 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37023
37023
  this.eventTracker = null;
37024
37024
  this.sdkFingerprint = null;
37025
37025
  this._onSecurityEvent = null;
37026
+ this.playlistNonce = null;
37027
+ this.nonceRefreshInProgress = false;
37026
37028
  this._onRateChange = this.enforceMaxRate.bind(this);
37027
37029
  this.videoElement = videoElement;
37028
37030
  this.config = {
@@ -37122,6 +37124,10 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37122
37124
  this.embedToken = await this.fetchEmbedToken();
37123
37125
  this.log('Embed token received', this.embedToken);
37124
37126
  await this.fetchAndApplyWatermark();
37127
+ if (this.embedToken.forensicWatermark) {
37128
+ this.playlistNonce = await this.acquireNonce(this.embedToken.tokenId);
37129
+ this.log('Initial playlist nonce: ' + (this.playlistNonce ? 'acquired' : 'skipped'));
37130
+ }
37125
37131
  await this.initializePlayer();
37126
37132
  }
37127
37133
  catch (err) {
@@ -37226,6 +37232,77 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37226
37232
  }
37227
37233
  return await response.json();
37228
37234
  }
37235
+ async solveChallenge(tokenId) {
37236
+ if (!this.config.apiBaseUrl)
37237
+ return undefined;
37238
+ try {
37239
+ const url = this.config.apiBaseUrl + '/videos/challenge/' + encodeURIComponent(tokenId);
37240
+ const resp = await fetch(url, { credentials: 'omit' });
37241
+ if (!resp.ok) {
37242
+ this.log('Challenge fetch failed (challenge may be disabled)', resp.status);
37243
+ return undefined;
37244
+ }
37245
+ const challenge = await resp.json();
37246
+ this.log('Challenge received, computing PBKDF2 proof-of-work...');
37247
+ const encoder = new TextEncoder();
37248
+ const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(challenge.nonce), 'PBKDF2', false, ['deriveBits']);
37249
+ await crypto.subtle.deriveBits({
37250
+ name: 'PBKDF2',
37251
+ salt: encoder.encode(challenge.salt),
37252
+ iterations: challenge.iterations,
37253
+ hash: 'SHA-256',
37254
+ }, keyMaterial, challenge.keyLength * 8);
37255
+ this.log('Challenge proof-of-work completed');
37256
+ return { nonce: challenge.nonce };
37257
+ }
37258
+ catch (err) {
37259
+ this.log('Challenge computation failed (non-fatal):', err);
37260
+ return undefined;
37261
+ }
37262
+ }
37263
+ async acquireNonce(tokenId) {
37264
+ if (!this.config.apiBaseUrl)
37265
+ return null;
37266
+ try {
37267
+ const challengeResult = await this.solveChallenge(tokenId);
37268
+ const url = this.config.apiBaseUrl + '/videos/playlist-session';
37269
+ const body = { tokenId };
37270
+ if (challengeResult) {
37271
+ body.challengeNonce = challengeResult.nonce;
37272
+ }
37273
+ const resp = await fetch(url, {
37274
+ method: 'POST',
37275
+ headers: { 'Content-Type': 'application/json' },
37276
+ body: JSON.stringify(body),
37277
+ credentials: 'omit',
37278
+ });
37279
+ if (!resp.ok) {
37280
+ this.log('Playlist session nonce acquisition failed', resp.status);
37281
+ return null;
37282
+ }
37283
+ const data = await resp.json();
37284
+ this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
37285
+ return data.nonce;
37286
+ }
37287
+ catch (err) {
37288
+ this.log('Nonce acquisition error (non-fatal):', err);
37289
+ return null;
37290
+ }
37291
+ }
37292
+ async refreshNonce() {
37293
+ if (this.nonceRefreshInProgress)
37294
+ return;
37295
+ if (!this.embedToken)
37296
+ return;
37297
+ 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
+ }
37305
+ }
37229
37306
  async initializePlayer() {
37230
37307
  if (!this.embedToken) {
37231
37308
  throw new Error('No embed token available');
@@ -37258,10 +37335,28 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37258
37335
  }
37259
37336
  initializeHls(playerUrl) {
37260
37337
  this.log('Using HLS.js for adaptive streaming');
37338
+ const self = this;
37261
37339
  this.hls = new Hls({
37262
37340
  debug: this.config.debug,
37263
37341
  enableWorker: true,
37264
37342
  lowLatencyMode: false,
37343
+ liveSyncDurationCount: 3,
37344
+ manifestLoadingMaxRetry: 6,
37345
+ levelLoadingMaxRetry: 6,
37346
+ xhrSetup(xhr, url) {
37347
+ if (url.includes('/watermark-stream/') && url.includes('.m3u8')) {
37348
+ if (self.playlistNonce) {
37349
+ const separator = url.includes('?') ? '&' : '?';
37350
+ const nonceUrl = url + separator + 'nonce=' + encodeURIComponent(self.playlistNonce);
37351
+ xhr.open('GET', nonceUrl, true);
37352
+ self.log('Injected nonce into playlist request');
37353
+ self.playlistNonce = null;
37354
+ self.refreshNonce().catch(() => {
37355
+ self.log('Background nonce refresh failed');
37356
+ });
37357
+ }
37358
+ }
37359
+ },
37265
37360
  ...this.config.hlsConfig,
37266
37361
  });
37267
37362
  this.hls.loadSource(playerUrl);
@@ -37288,10 +37383,24 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37288
37383
  });
37289
37384
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
37290
37385
  this.error('HLS Error', data);
37386
+ if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
37387
+ data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
37388
+ this.embedToken?.forensicWatermark) {
37389
+ this.log('Playlist load failed — refreshing nonce before retry');
37390
+ this.refreshNonce().then(() => {
37391
+ setTimeout(() => this.hls?.startLoad(), 500);
37392
+ }).catch(() => {
37393
+ setTimeout(() => this.hls?.startLoad(), 1000);
37394
+ });
37395
+ return;
37396
+ }
37291
37397
  if (data.fatal) {
37292
37398
  switch (data.type) {
37293
37399
  case Hls.ErrorTypes.NETWORK_ERROR:
37294
37400
  this.error('Network error, attempting recovery...');
37401
+ if (this.embedToken?.forensicWatermark) {
37402
+ this.refreshNonce().catch(() => { });
37403
+ }
37295
37404
  setTimeout(() => this.hls?.startLoad(), 1000);
37296
37405
  break;
37297
37406
  case Hls.ErrorTypes.MEDIA_ERROR: