senza-sdk 4.5.2 → 4.5.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senza-sdk",
3
- "version": "4.5.2",
3
+ "version": "4.5.4",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
package/src/api.js CHANGED
@@ -153,12 +153,13 @@ export const envInfo = {
153
153
  * @param {ArrayBuffer|string} licenseResponse a license response that was received from the license server to be passed to platform.
154
154
  * @param {string} fcid a fcid received with the license request
155
155
  * @param {string} sessionId a sessionId received with the license request
156
- * In case of success licenceResponse is of type @type {ArrayBuffer}
156
+ * @param {string} playbackId the current playbackId, propagated to platform for session correlation
157
+ * In case of success licenseResponse is of type @type {ArrayBuffer}
157
158
  * In case of error licenseResponse is of type @type {string}
158
159
  */
159
- export function writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId) {
160
+ export function writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId, playbackId) {
160
161
  if (window?.senzaSDKImplementation) {
161
- return window.senzaSDKImplementation.writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId);
162
+ return window.senzaSDKImplementation.writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId, playbackId);
162
163
  }
163
164
  sdkLogger.error("writeLicenseResponse: window.cefQuery is undefined");
164
165
  }
@@ -303,10 +303,11 @@ import "./devHelper.js";
303
303
  * @param {ArrayBuffer|string} licenseResponse a license response that was received from the license server to be passed to platform.
304
304
  * @param {string} fcid a fcid received with the license request
305
305
  * @param {string} sessionId a sessionId received with the license request
306
- * In case of success licenceResponse is of type @type {ArrayBuffer}
306
+ * @param {string} playbackId the current playbackId, propagated to platform for session correlation
307
+ * In case of success licenseResponse is of type @type {ArrayBuffer}
307
308
  * In case of error licenseResponse is of type @type {string}
308
309
  */
309
- export function writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId) {
310
+ export function writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId, playbackId) {
310
311
 
311
312
  if (statusCode >= 200 && statusCode < 300) {
312
313
  licenseResponse = window.btoa(String.fromCharCode.apply(null, new Uint8Array(licenseResponse))); // to base64
@@ -316,7 +317,8 @@ export function writeLicenseResponse(statusCode, licenseResponse, fcid, sessionI
316
317
  const message = {
317
318
  type: "updateLicense",
318
319
  sessionId,
319
- fcid
320
+ fcid,
321
+ playbackId
320
322
  };
321
323
  message[statusCode >= 200 && statusCode < 300 ? "response" : "error"] = licenseResponse;
322
324
  const request = { target: "TC", waitForResponse: false, message: JSON.stringify(message) };
@@ -22,6 +22,8 @@ const OVERLAY_CAPTURE_PRESETS = {
22
22
 
23
23
  const DEFAULT_CAPTURE_PRESET = "default";
24
24
  const VALID_QUALITIES = new Set(["low", "mid", "high"]);
25
+ const DEFAULT_AUTO_HIDE_DURATION_SEC = 600;
26
+ const OVERLAY_DURATION_SEC_MAX = 65535;
25
27
 
26
28
  class Overlay extends OverlayInterface {
27
29
  constructor() {
@@ -31,7 +33,8 @@ class Overlay extends OverlayInterface {
31
33
  this._configuration = {
32
34
  useTransparency: true,
33
35
  overlayCapturePreset: DEFAULT_CAPTURE_PRESET,
34
- overlayCapturePlan: null
36
+ overlayCapturePlan: null,
37
+ autoHideDurationSec: DEFAULT_AUTO_HIDE_DURATION_SEC
35
38
  };
36
39
  this._activeCapturePlan = OVERLAY_CAPTURE_PRESETS[DEFAULT_CAPTURE_PRESET];
37
40
  this._retryGeneration = 0;
@@ -138,6 +141,7 @@ class Overlay extends OverlayInterface {
138
141
  quality,
139
142
  conditional
140
143
  };
144
+ message.overlayDurationSec = this._configuration.autoHideDurationSec;
141
145
  sdkLogger.log(`Overlay: rendering frame x=${x} y=${y} width=${width} height=${height} fcid=${FCID} (batchId=${batchId}, quality=${quality}, conditional=${conditional})`);
142
146
  const request = { target: "UI-Streamer", waitForResponse: false, message: JSON.stringify(message) };
143
147
  window.cefQuery({
@@ -226,6 +230,16 @@ class Overlay extends OverlayInterface {
226
230
  }
227
231
  }
228
232
 
233
+ if (Object.prototype.hasOwnProperty.call(normalizedConfiguration, "autoHideDurationSec")) {
234
+ const duration = normalizedConfiguration.autoHideDurationSec;
235
+ if (Number.isInteger(duration) && duration >= 0 && duration <= OVERLAY_DURATION_SEC_MAX) {
236
+ normalizedConfiguration.autoHideDurationSec = duration;
237
+ } else {
238
+ sdkLogger.warn(`Overlay: invalid autoHideDurationSec "${duration}", keeping previous value`);
239
+ delete normalizedConfiguration.autoHideDurationSec;
240
+ }
241
+ }
242
+
229
243
  this._configuration = { ...this._configuration, ...normalizedConfiguration };
230
244
  this._activeCapturePlan = this._resolveCapturePlan();
231
245
  }
@@ -2,6 +2,7 @@
2
2
  import { RemotePlayer as RemotePlayerInterface, RemotePlayerError as RemotePlayerErrorInterface, Config } from "../interface/remotePlayer";
3
3
  import {
4
4
  getFCID,
5
+ generatePlaybackId,
5
6
  isAudioSyncConfigured,
6
7
  clearTimer,
7
8
  sdkLogger,
@@ -145,6 +146,13 @@ class RemotePlayer extends RemotePlayerInterface {
145
146
  * @private
146
147
  */
147
148
  this._isPlaying = false;
149
+
150
+ /**
151
+ * @type {string}
152
+ * @description Unique identifier generated on every load attempt. Propagated on all API requests and events.
153
+ * @private
154
+ */
155
+ this._playbackId = "";
148
156
  }
149
157
 
150
158
  /** @private Initialize the remote player
@@ -219,17 +227,33 @@ class RemotePlayer extends RemotePlayerInterface {
219
227
 
220
228
  }
221
229
 
230
+ /**
231
+ * @private Returns true and logs an error when an incoming event's playbackId does not match the current one.
232
+ * @param {Object} detail - The event detail object that may contain a playbackId field.
233
+ * @param {string} eventType - Name of the incoming event, used in the log message.
234
+ */
235
+ _hasPlaybackIdMismatch(detail, eventType) {
236
+ const incomingId = detail?.playbackId;
237
+ if (incomingId && this._playbackId && incomingId !== this._playbackId) {
238
+ sdkLogger.error(`${eventType}: playbackId mismatch. current=${this._playbackId} incoming=${incomingId}. Ignoring.`);
239
+ return true;
240
+ }
241
+ return false;
242
+ }
243
+
222
244
  /**
223
245
  * @private Add event listeners for system events
224
246
  */
225
247
  _addSenzaEventListeners() {
226
248
 
227
249
  typeof document !== "undefined" && document.addEventListener("hs/remotePlayerEvent", (e) => {
250
+ if (this._hasPlaybackIdMismatch(e?.detail, "hs/remotePlayerEvent")) return;
228
251
  sdkLogger.info("Got hs/remotePlayerEvent event with detail", JSON.stringify(e?.detail));
229
252
  this.dispatchEvent(new Event(e?.detail?.eventName));
230
253
  });
231
254
 
232
- typeof document !== "undefined" && document.addEventListener("hs/playbackInfoEvent", () => {
255
+ typeof document !== "undefined" && document.addEventListener("hs/playbackInfoEvent", (e) => {
256
+ if (this._hasPlaybackIdMismatch(e?.detail, "hs/playbackInfoEvent")) return;
233
257
  sdkLogger.info("Got hs/playbackInfoEvent");
234
258
  // When attached, the sdk controls the synchronization between the local and remote player.
235
259
  if (this._videoElement) {
@@ -243,13 +267,15 @@ class RemotePlayer extends RemotePlayerInterface {
243
267
  });
244
268
 
245
269
  typeof document !== "undefined" && document.addEventListener("hs/playback", (e) => {
270
+ if (this._hasPlaybackIdMismatch(e?.detail, "hs/playback")) return;
246
271
  sdkLogger.info("Got hs/playback event with detail", JSON.stringify(e?.detail));
247
272
  this._availabilityStartTime = e?.detail?.availabilityStartTime;
248
273
  this._updateTracks(e?.detail);
249
274
  this.dispatchEvent(new Event("tracksupdate"));
250
275
  });
251
276
 
252
- typeof document !== "undefined" && document.addEventListener("hs/EOS", () => {
277
+ typeof document !== "undefined" && document.addEventListener("hs/EOS", (e) => {
278
+ if (this._hasPlaybackIdMismatch(e?.detail, "hs/EOS")) return;
253
279
  sdkLogger.info("Got hs/EOS event");
254
280
  this._changePlayMode(false);
255
281
  this.dispatchEvent(new Event("ended"));
@@ -268,6 +294,7 @@ class RemotePlayer extends RemotePlayerInterface {
268
294
  }
269
295
  });
270
296
  typeof document !== "undefined" && document.addEventListener("hs/senzaPlayerSetRate", (event) => {
297
+ if (this._hasPlaybackIdMismatch(event?.detail, "hs/senzaPlayerSetRate")) return;
271
298
  if (!this._videoElement) return;
272
299
  if (this._isSeekingByApplication) {
273
300
  sdkLogger.info("Skip senzaPlayerSetRate while seek performed");
@@ -276,6 +303,7 @@ class RemotePlayer extends RemotePlayerInterface {
276
303
  this._videoElement.playbackRate = event.detail.rate;
277
304
  });
278
305
  typeof document !== "undefined" && document.addEventListener("hs/senzaPlayerSetTime", (event) => {
306
+ if (this._hasPlaybackIdMismatch(event?.detail, "hs/senzaPlayerSetTime")) return;
279
307
  // For simplicity, make sure we only accept syncing the current time if audio sync is enabled
280
308
  if (!this._isAudioSyncEnabled()) return;
281
309
  if (this._isSeekingByApplication) {
@@ -291,6 +319,7 @@ class RemotePlayer extends RemotePlayerInterface {
291
319
 
292
320
 
293
321
  typeof document !== "undefined" && document.addEventListener("hs/ERR", (event) => {
322
+ if (this._hasPlaybackIdMismatch(event?.detail, "hs/ERR")) return;
294
323
  sdkLogger.info("Got hs/ERR event");
295
324
  delete event?.detail?.type; // type is always videoPlaybackEvent, so no need to pass it
296
325
  delete event?.detail?.eventCode; // eventCode is always ERR, so no need to pass it
@@ -298,6 +327,7 @@ class RemotePlayer extends RemotePlayerInterface {
298
327
  });
299
328
 
300
329
  typeof document !== "undefined" && document.addEventListener("hs/getLicense", (event) => {
330
+ if (this._hasPlaybackIdMismatch(event?.detail, "hs/getLicense")) return;
301
331
  sdkLogger.info("Got hs/getLicense event");
302
332
  const getLicenseEventData = event?.detail;
303
333
 
@@ -308,9 +338,10 @@ class RemotePlayer extends RemotePlayerInterface {
308
338
  this.licenseRequest = licenseRequest;
309
339
  const fcid = getLicenseEventData.fcid;
310
340
  const sessionId = getLicenseEventData.sessionId;
341
+ const playbackId = this._playbackId;
311
342
  const licenseRequestEvent = new CustomEvent("license-request", { "detail": { licenseRequest } });
312
343
  licenseRequestEvent.writeLicenseResponse = (statusCode, responseBody) => {
313
- writeLicenseResponse(statusCode, responseBody, fcid, sessionId);
344
+ writeLicenseResponse(statusCode, responseBody, fcid, sessionId, playbackId);
314
345
  };
315
346
  this.dispatchEvent(licenseRequestEvent);
316
347
  });
@@ -485,7 +516,8 @@ class RemotePlayer extends RemotePlayerInterface {
485
516
  class: "remotePlayer",
486
517
  action: "seek",
487
518
  playbackPosition,
488
- fcid: FCID
519
+ fcid: FCID,
520
+ playbackId: this._playbackId
489
521
  };
490
522
  const request = { target: "TC", waitForResponse: waitForResponse, message: JSON.stringify(message) };
491
523
  return new Promise((resolve, reject) => {
@@ -544,7 +576,8 @@ class RemotePlayer extends RemotePlayerInterface {
544
576
  fcid: FCID,
545
577
  audioLanguage,
546
578
  subtitlesLanguage,
547
- playbackPosition: this.currentTime
579
+ playbackPosition: this.currentTime,
580
+ playbackId: this._playbackId
548
581
  };
549
582
  let waitForResponse = false;
550
583
  if (this._remotePlayerApiVersion >= 2) {
@@ -611,7 +644,8 @@ class RemotePlayer extends RemotePlayerInterface {
611
644
  type: "remotePlayer.pause",
612
645
  class: "remotePlayer",
613
646
  action: "pause",
614
- fcid: FCID
647
+ fcid: FCID,
648
+ playbackId: this._playbackId
615
649
  };
616
650
  if (this._remotePlayerApiVersion >= 2) {
617
651
  message.streamType = this._isAudioSyncEnabled() && isForegroundState ? StreamType.AUDIO : StreamType.AUDIO | StreamType.VIDEO | StreamType.SUBTITLE;
@@ -662,7 +696,8 @@ class RemotePlayer extends RemotePlayerInterface {
662
696
  class: "remotePlayer",
663
697
  action: "stop",
664
698
  streamType: streamType,
665
- fcid: FCID
699
+ fcid: FCID,
700
+ playbackId: this._playbackId
666
701
  };
667
702
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
668
703
  return new Promise((resolve, reject) => {
@@ -784,6 +819,7 @@ class RemotePlayer extends RemotePlayerInterface {
784
819
  this._abortSeeking = true;
785
820
  if (reset) {
786
821
  this._reset();
822
+ this._playbackId = generatePlaybackId();
787
823
  }
788
824
  const previousLoadMode = this._loadMode;
789
825
  this._changeLoadMode(this.LoadMode.LOADING);
@@ -804,7 +840,8 @@ class RemotePlayer extends RemotePlayerInterface {
804
840
  timeout: this._remotePlayerConfirmationTimeout,
805
841
  autoPlay: false,
806
842
  playbackPosition,
807
- fcid: FCID
843
+ fcid: FCID,
844
+ playbackId: this._playbackId
808
845
  };
809
846
  if (this._uiAvSyncIntervalMs !== undefined) {
810
847
  message.connectorSettings = message.connectorSettings || {};
@@ -905,7 +942,8 @@ class RemotePlayer extends RemotePlayerInterface {
905
942
  type: "remotePlayer.unload",
906
943
  class: "remotePlayer",
907
944
  action: "unload",
908
- fcid: FCID
945
+ fcid: FCID,
946
+ playbackId: this._playbackId
909
947
  };
910
948
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
911
949
  let timerId = 0;
@@ -918,6 +956,7 @@ class RemotePlayer extends RemotePlayerInterface {
918
956
  logger.withFields({ duration }).log(`unload completed successfully after ${duration} ms`);
919
957
  timerId = clearTimer(timerId);
920
958
  this._reset();
959
+ this._playbackId = "";
921
960
  this._changeLoadMode(this.LoadMode.NOT_LOADED);
922
961
  this._loadedUrl = undefined;
923
962
  resolve();
@@ -1240,7 +1279,8 @@ class RemotePlayer extends RemotePlayerInterface {
1240
1279
  class: "remotePlayer",
1241
1280
  action: "setAudioLanguage",
1242
1281
  fcid: FCID,
1243
- language: audioTrackId
1282
+ language: audioTrackId,
1283
+ playbackId: this._playbackId
1244
1284
  };
1245
1285
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
1246
1286
  return new Promise((resolve, reject) => {
@@ -1387,7 +1427,8 @@ class RemotePlayer extends RemotePlayerInterface {
1387
1427
  class: "remotePlayer",
1388
1428
  action: "setSubtitleLanguage",
1389
1429
  fcid: FCID,
1390
- language: textTrackId
1430
+ language: textTrackId,
1431
+ playbackId: this._playbackId
1391
1432
  };
1392
1433
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
1393
1434
  return new Promise((resolve, reject) => {
@@ -1889,7 +1930,8 @@ class RemotePlayer extends RemotePlayerInterface {
1889
1930
  class: "remotePlayer",
1890
1931
  action: "screenBlackout",
1891
1932
  fcid: FCID,
1892
- blackoutTime
1933
+ blackoutTime,
1934
+ playbackId: this._playbackId
1893
1935
  };
1894
1936
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
1895
1937
  return new Promise((resolve, reject) => {
@@ -14,6 +14,19 @@ export function getFCID() {
14
14
  return Math.round(Math.random() * 100000) + "-" + getPlatformInfo().sessionInfo?.connectionId;
15
15
  }
16
16
 
17
+ export function generatePlaybackId() {
18
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
19
+ const size = 5;
20
+ let id = "";
21
+ const bytes = (typeof crypto !== "undefined" && crypto.getRandomValues)
22
+ ? crypto.getRandomValues(new Uint8Array(size))
23
+ : Array.from({ length: size }, () => Math.floor(Math.random() * 256));
24
+ for (let i = 0; i < size; i++) {
25
+ id += alphabet[bytes[i] & 63];
26
+ }
27
+ return id;
28
+ }
29
+
17
30
  export class SenzaError extends Error {
18
31
  constructor(code, message) {
19
32
  super(message);
@@ -59,8 +59,8 @@ class Overlay extends EventTarget {
59
59
  }
60
60
 
61
61
  /**
62
- * Removes all registered elements, sends a zero-dimension frame to the UI-Streamer,
63
- * and deactivates the overlay. After calling removeAllElements(), a new element can be registered.
62
+ * Removes all registered elements and deactivates the overlay.
63
+ * After calling removeAllElements(), a new element can be registered.
64
64
  *
65
65
  * @returns {Promise<boolean>} Resolves to true if successful, rejects with error if failed.
66
66
  */
@@ -82,7 +82,7 @@ class Overlay extends EventTarget {
82
82
  *
83
83
  * @param {Object} configuration - The new configuration to apply.
84
84
  * @param {boolean} [configuration.useTransparency=true] - Controls whether the overlay should be rendered with transparency.
85
- * When set to `true`, the value is forwarded to UI-Streamer in `displayOverlay` requests.
85
+ * When set to `true`, the overlay is rendered with transparency.
86
86
  * @param {string} [configuration.overlayCapturePreset="default"] - Named capture plan preset:
87
87
  * `default` (progressive low→high captures) or `once` (one immediate high-quality conditional step).
88
88
  * @param {Array<{quality: "low"|"mid"|"high", conditional: boolean, delay: number}>} [configuration.overlayCapturePlan]
@@ -90,6 +90,9 @@ class Overlay extends EventTarget {
90
90
  * the previous step in the batch. Set `conditional` per step (the SDK does not derive it):
91
91
  * when `true`, capture only if overlay content changed since the last required (non-conditional)
92
92
  * capture in this batch; when `false`, always capture on that step.
93
+ * @param {number} [configuration.autoHideDurationSec=600] - Client auto-hide duration in seconds.
94
+ * Defaults to `600` (10 minutes) until configured. Omit the key to keep the current value.
95
+ * `0` means never auto-hide. Valid range: `0`–`65535`.
93
96
  */
94
97
  configure(configuration) {
95
98
  noop(configuration);
@@ -1 +1 @@
1
- export const version = "4.5.2";
1
+ export const version = "4.5.4";