@guardvideo/player-sdk 3.2.0 → 3.3.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,79 @@ 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 response...');
37247
+ const encoder = new TextEncoder();
37248
+ const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(challenge.nonce), 'PBKDF2', false, ['deriveBits']);
37249
+ const derivedBits = 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
+ 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;
37259
+ }
37260
+ catch (err) {
37261
+ this.log('Challenge computation failed (non-fatal):', err);
37262
+ return undefined;
37263
+ }
37264
+ }
37265
+ async acquireNonce(tokenId) {
37266
+ if (!this.config.apiBaseUrl)
37267
+ return null;
37268
+ try {
37269
+ const challengeResponse = await this.solveChallenge(tokenId);
37270
+ const url = this.config.apiBaseUrl + '/videos/playlist-session';
37271
+ const body = { tokenId };
37272
+ if (challengeResponse) {
37273
+ body.challengeResponse = challengeResponse;
37274
+ }
37275
+ const resp = await fetch(url, {
37276
+ method: 'POST',
37277
+ headers: { 'Content-Type': 'application/json' },
37278
+ body: JSON.stringify(body),
37279
+ credentials: 'omit',
37280
+ });
37281
+ if (!resp.ok) {
37282
+ this.log('Playlist session nonce acquisition failed', resp.status);
37283
+ return null;
37284
+ }
37285
+ const data = await resp.json();
37286
+ this.log('Playlist nonce acquired, expires in ' + data.expiresIn + 's');
37287
+ return data.nonce;
37288
+ }
37289
+ catch (err) {
37290
+ this.log('Nonce acquisition error (non-fatal):', err);
37291
+ return null;
37292
+ }
37293
+ }
37294
+ async refreshNonce() {
37295
+ if (this.nonceRefreshInProgress)
37296
+ return;
37297
+ if (!this.embedToken)
37298
+ return;
37299
+ 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
+ }
37307
+ }
37229
37308
  async initializePlayer() {
37230
37309
  if (!this.embedToken) {
37231
37310
  throw new Error('No embed token available');
@@ -37258,10 +37337,28 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37258
37337
  }
37259
37338
  initializeHls(playerUrl) {
37260
37339
  this.log('Using HLS.js for adaptive streaming');
37340
+ const self = this;
37261
37341
  this.hls = new Hls({
37262
37342
  debug: this.config.debug,
37263
37343
  enableWorker: true,
37264
37344
  lowLatencyMode: false,
37345
+ liveSyncDurationCount: 3,
37346
+ manifestLoadingMaxRetry: 6,
37347
+ levelLoadingMaxRetry: 6,
37348
+ xhrSetup(xhr, url) {
37349
+ if (url.includes('/watermark-stream/') && url.includes('.m3u8')) {
37350
+ if (self.playlistNonce) {
37351
+ const separator = url.includes('?') ? '&' : '?';
37352
+ const nonceUrl = url + separator + 'nonce=' + encodeURIComponent(self.playlistNonce);
37353
+ xhr.open('GET', nonceUrl, true);
37354
+ self.log('Injected nonce into playlist request');
37355
+ self.playlistNonce = null;
37356
+ self.refreshNonce().catch(() => {
37357
+ self.log('Background nonce refresh failed');
37358
+ });
37359
+ }
37360
+ }
37361
+ },
37265
37362
  ...this.config.hlsConfig,
37266
37363
  });
37267
37364
  this.hls.loadSource(playerUrl);
@@ -37288,10 +37385,24 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37288
37385
  });
37289
37386
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
37290
37387
  this.error('HLS Error', data);
37388
+ if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
37389
+ data.details === Hls.ErrorDetails.LEVEL_LOAD_ERROR &&
37390
+ this.embedToken?.forensicWatermark) {
37391
+ this.log('Playlist load failed — refreshing nonce before retry');
37392
+ this.refreshNonce().then(() => {
37393
+ setTimeout(() => this.hls?.startLoad(), 500);
37394
+ }).catch(() => {
37395
+ setTimeout(() => this.hls?.startLoad(), 1000);
37396
+ });
37397
+ return;
37398
+ }
37291
37399
  if (data.fatal) {
37292
37400
  switch (data.type) {
37293
37401
  case Hls.ErrorTypes.NETWORK_ERROR:
37294
37402
  this.error('Network error, attempting recovery...');
37403
+ if (this.embedToken?.forensicWatermark) {
37404
+ this.refreshNonce().catch(() => { });
37405
+ }
37295
37406
  setTimeout(() => this.hls?.startLoad(), 1000);
37296
37407
  break;
37297
37408
  case Hls.ErrorTypes.MEDIA_ERROR: