@guardvideo/player-sdk 3.1.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 = {
@@ -37121,8 +37123,12 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37121
37123
  this.setState(exports.PlayerState.LOADING);
37122
37124
  this.embedToken = await this.fetchEmbedToken();
37123
37125
  this.log('Embed token received', this.embedToken);
37124
- await this.initializePlayer();
37125
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
+ }
37131
+ await this.initializePlayer();
37126
37132
  }
37127
37133
  catch (err) {
37128
37134
  this.handleError({
@@ -37155,6 +37161,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37155
37161
  if (cfg.enableWatermark && cfg.watermarkText) {
37156
37162
  this.config.onWatermark?.(cfg.watermarkText);
37157
37163
  }
37164
+ this.sessionId = cfg.sessionId ?? undefined;
37158
37165
  if (this.config.trackViewerEvents && cfg.eventsSecret) {
37159
37166
  this.initEventTracker(cfg.eventsSecret, cfg.sessionId ?? undefined);
37160
37167
  }
@@ -37225,11 +37232,89 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37225
37232
  }
37226
37233
  return await response.json();
37227
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
+ }
37228
37308
  async initializePlayer() {
37229
37309
  if (!this.embedToken) {
37230
37310
  throw new Error('No embed token available');
37231
37311
  }
37232
- const playerUrl = this.embedToken.playerUrl;
37312
+ const basePlayerUrl = this.embedToken.playerUrl;
37313
+ const playerUrl = this.sessionId
37314
+ ? (basePlayerUrl.includes('?')
37315
+ ? `${basePlayerUrl}&session=${encodeURIComponent(this.sessionId)}`
37316
+ : `${basePlayerUrl}?session=${encodeURIComponent(this.sessionId)}`)
37317
+ : basePlayerUrl;
37233
37318
  this.log('Initializing player with URL', playerUrl);
37234
37319
  this.videoElement.controls = this.config.controls;
37235
37320
  if (this.config.className) {
@@ -37252,10 +37337,28 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37252
37337
  }
37253
37338
  initializeHls(playerUrl) {
37254
37339
  this.log('Using HLS.js for adaptive streaming');
37340
+ const self = this;
37255
37341
  this.hls = new Hls({
37256
37342
  debug: this.config.debug,
37257
37343
  enableWorker: true,
37258
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
+ },
37259
37362
  ...this.config.hlsConfig,
37260
37363
  });
37261
37364
  this.hls.loadSource(playerUrl);
@@ -37282,10 +37385,24 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37282
37385
  });
37283
37386
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
37284
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
+ }
37285
37399
  if (data.fatal) {
37286
37400
  switch (data.type) {
37287
37401
  case Hls.ErrorTypes.NETWORK_ERROR:
37288
37402
  this.error('Network error, attempting recovery...');
37403
+ if (this.embedToken?.forensicWatermark) {
37404
+ this.refreshNonce().catch(() => { });
37405
+ }
37289
37406
  setTimeout(() => this.hls?.startLoad(), 1000);
37290
37407
  break;
37291
37408
  case Hls.ErrorTypes.MEDIA_ERROR: