senza-sdk 4.4.4 → 4.4.5

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.4.4",
3
+ "version": "4.4.5",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
@@ -23,17 +23,18 @@
23
23
  "version:output": "echo 'export const version = \"'$npm_package_version'\";' > src/interface/version.js"
24
24
  },
25
25
  "devDependencies": {
26
- "@babel/cli": "^7.13.16",
27
- "@babel/core": "7.18.6",
28
- "@babel/plugin-transform-modules-commonjs": "^7.16.8",
29
- "@babel/preset-env": "^7.14.1",
26
+ "@babel/cli": "^7.22.0",
27
+ "@babel/core": "^7.22.0",
28
+ "@babel/plugin-transform-modules-commonjs": "^7.22.0",
29
+ "@babel/preset-env": "^7.22.0",
30
30
  "@eslint/eslintrc": "^3.3.0",
31
31
  "@eslint/js": "^9.22.0",
32
- "babel-jest": "^27.5.1",
32
+ "babel-jest": "^30.2.0",
33
33
  "eslint": "^9.22.0",
34
34
  "eslint-plugin-jest": "^28.11.0",
35
35
  "globals": "^16.0.0",
36
- "jest": "^27.5.1",
36
+ "jest": "^30.2.0",
37
+ "jest-environment-jsdom" : "^30.2.0",
37
38
  "jsdoc-to-markdown": "^7.1.1",
38
39
  "webpack": "^5.72.1",
39
40
  "webpack-cli": "^5.1.4"
@@ -6,6 +6,7 @@ import { remotePlayer } from "./remotePlayer.js";
6
6
  import { deviceManager } from "./deviceManager.js";
7
7
  import { displayManager } from "./displayManager.js";
8
8
  import { messageManager } from "./messageManager.js";
9
+ import { backgroundRenderControl } from "./backgroundRenderControl.js";
9
10
 
10
11
  export { remotePlayer };
11
12
  export { lifecycle };
@@ -141,6 +142,7 @@ export async function init(interfaceApiVersion, showSequenceFunc, initSequenceFu
141
142
  await remotePlayer._init(sessionInfoObj, triggerEvent);
142
143
  alarmManager._init();
143
144
  messageManager._init();
145
+ backgroundRenderControl._init();
144
146
  await displayManager._init();
145
147
  sdkLogger.log("All submodules initialized");
146
148
 
@@ -0,0 +1,113 @@
1
+ import { lifecycle } from "./lifecycle.js";
2
+ import { sdkLogger } from "./utils.js";
3
+ import { sessionInfo } from "./SessionInfo";
4
+
5
+ /**
6
+ * Internal background render control.
7
+ * Listens to lifecycle state changes and disables animation / video rendering when in background mode.
8
+ * Specifically it pauses CSS animations and hides video elements to reduce CPU/GPU load
9
+ *
10
+ * For remote-gpu deployments this reduces the CPU use in background mode from 6 cores to 0.4 cores.
11
+ *
12
+ * Extra CPU can be saved by also overriding requestAnimationFrame and slowing down time.
13
+ * This has the effect of ensuring the segments are downloaded at a slower pace
14
+ * - or not at all if you actually pause video, but these may have more complicated state and audio sync issues.
15
+ * Either way if you slow the decoding speed then we save more CPU by not doing as much SW video decoding,
16
+ * but the bulk is saved by the hidden video elements.
17
+ * See comments on https://jira01.engit.synamedia.com/browse/HSDEV-16827 for more about a PoC / spike that did this.
18
+ *
19
+ * As it stands, the current implementation that gets us 0.4 cores in background mode for remote-gpu is a good compromise of complexity vs benefit.
20
+ * It also roughly matches GPU mode background CPU use (maybe slightly better).
21
+ * GPU mode with this feature enabled (from my testing) saves about 0.15 cores ~0.45 -> ~0.30 cores
22
+ *
23
+ * This module uses the following settings:
24
+ * {
25
+ * "ui-streamer": {
26
+ * "throttleRenderingInBackground": {
27
+ * "enabled": true, // enable or disable the feature
28
+ * }
29
+ * }
30
+ * }
31
+ */
32
+ class BackgroundRenderControl {
33
+
34
+ /** initialised
35
+ * @type {boolean}
36
+ * @private
37
+ * Whether the instance has been initialized.
38
+ */
39
+ _initialized = false;
40
+
41
+ /**
42
+ * @type {boolean}
43
+ * @private
44
+ * Whether render control is enabled.
45
+ */
46
+ _enabled = false;
47
+
48
+ /**
49
+ * @type {boolean}
50
+ * @private
51
+ * Whether rendering is currently active.
52
+ */
53
+ _active = true;
54
+
55
+ /** @type {string|null}
56
+ * @private
57
+ * Stores the last known lifecycle state.
58
+ */
59
+ _lastState = null;
60
+
61
+ /** @type {Function}
62
+ * @private
63
+ * Event listener for lifecycle state changes.
64
+ */
65
+ _listener = (e) => this._handleStateChange(e.state);
66
+
67
+ _init() {
68
+ if (this._initialized) return;
69
+ this._enabled = sessionInfo?.sessionInfoObj.settings?.["ui-streamer"]?.throttleRenderingInBackground?.enabled ?? false;
70
+ sdkLogger.log(`Initializing BackgroundRenderControl: enabled=${this._enabled}`);
71
+ if (!this._enabled) return;
72
+ this._lastState = lifecycle.state;
73
+ lifecycle.addEventListener("onstatechange", this._listener);
74
+ this._initialized = true;
75
+ }
76
+
77
+ _handleStateChange(state) {
78
+ if (!this._enabled) return;
79
+ if (state === this._lastState) return;
80
+ this._lastState = state;
81
+ try {
82
+ if (state === lifecycle.UiState.FOREGROUND) this._onForeground();
83
+ else if (state === lifecycle.UiState.BACKGROUND) this._onBackground();
84
+ } catch (err) {
85
+ sdkLogger.error("BackgroundRenderControl state handling error", err);
86
+ }
87
+ }
88
+
89
+ _onForeground() {
90
+ if (this._active) return;
91
+ sdkLogger.log("BackgroundRenderControl resume rendering");
92
+ this._active = true;
93
+ const style = document.getElementById("hs-sdk-bg-render-styles");
94
+ if (style) style.remove();
95
+ }
96
+
97
+ _onBackground() {
98
+ if (!this._active) return;
99
+ sdkLogger.log("BackgroundRenderControl throttle rendering");
100
+ this._active = false;
101
+ if (!document.getElementById("hs-sdk-bg-render-styles")) {
102
+ const style = document.createElement("style");
103
+ style.id = "hs-sdk-bg-render-styles";
104
+ style.textContent = `
105
+ * { animation-play-state: paused !important; transition: none !important; scroll-behavior: auto !important; }
106
+ canvas, video, img { visibility: hidden !important; }
107
+ `;
108
+ document.head.appendChild(style);
109
+ }
110
+ }
111
+ }
112
+
113
+ export const backgroundRenderControl = new BackgroundRenderControl();
@@ -62,6 +62,12 @@ class Lifecycle extends LifecycleInterface {
62
62
  this._autoBackgroundOnUIDelay = DEFAULT_AUTO_BACKGROUND_UI_DELAY;
63
63
  }
64
64
 
65
+ // List of event types that should use the _eventManager
66
+ static _waitForListenersEvents = [
67
+ "userdisconnected","beforestatechange"
68
+ // Add more event types here if needed in the future
69
+ ];
70
+
65
71
  /**
66
72
  * @private Initialize the lifecycle
67
73
  * @param {Object} uiStreamerSettings - UI-streamer portion of the settings taken from session info
@@ -177,7 +183,7 @@ class Lifecycle extends LifecycleInterface {
177
183
  const event = new Event("onstatechange");
178
184
  event.state = e.detail;
179
185
  this._state = event.state;
180
- if (this._isAutoBackgroundEnabled() && this.state === this.UiState.FOREGROUND) {
186
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning()) {
181
187
  this._startCountdown();
182
188
  }
183
189
  this.dispatchEvent(event);
@@ -213,7 +219,7 @@ class Lifecycle extends LifecycleInterface {
213
219
 
214
220
  // Add playModeChange listener
215
221
  remotePlayer.addEventListener("playModeChange", (event) => {
216
- if (this._isAutoBackgroundEnabled() && this.state === this.UiState.FOREGROUND) {
222
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning()) {
217
223
  sdkLogger.log("Resetting auto background timer due to play mode change", event.detail.isPlaying);
218
224
  this._startCountdown(event.detail.isPlaying);
219
225
  }
@@ -221,6 +227,14 @@ class Lifecycle extends LifecycleInterface {
221
227
  sdkLogger.log("[Lifecycle] Added event listeners for system events");
222
228
  }
223
229
 
230
+ /**
231
+ * @private Checks if the UI is in foreground or transitioning to foreground.
232
+ * @returns {boolean}
233
+ */
234
+ _isForegroundOrTransitioning() {
235
+ return this._state === this.UiState.FOREGROUND || this._state === this.UiState.IN_TRANSITION_TO_FOREGROUND;
236
+ }
237
+
224
238
  /**
225
239
  * @private Checks if auto background is enabled including overrides.
226
240
  * @returns {boolean}
@@ -284,7 +298,7 @@ class Lifecycle extends LifecycleInterface {
284
298
  }
285
299
 
286
300
  // Start or stop countdown based on new configuration
287
- if (this._isAutoBackgroundEnabled()) {
301
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning()) {
288
302
  this._startCountdown();
289
303
  } else {
290
304
  this._stopCountdown();
@@ -364,7 +378,7 @@ class Lifecycle extends LifecycleInterface {
364
378
 
365
379
  set autoBackground(enabled) {
366
380
  this._autoBackground = enabled;
367
- if (this._isAutoBackgroundEnabled()) {
381
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning()) {
368
382
  this._startCountdown();
369
383
  } else {
370
384
  this._stopCountdown();
@@ -377,7 +391,7 @@ class Lifecycle extends LifecycleInterface {
377
391
 
378
392
  set autoBackgroundDelay(delay) {
379
393
  this._autoBackgroundOnVideoDelay = delay;
380
- if (this._isAutoBackgroundEnabled() && remotePlayer._isPlaying) {
394
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning() && remotePlayer._isPlaying) {
381
395
  this._startCountdown();
382
396
  }
383
397
  }
@@ -388,7 +402,7 @@ class Lifecycle extends LifecycleInterface {
388
402
 
389
403
  set autoBackgroundOnUIDelay(delay) {
390
404
  this._autoBackgroundOnUIDelay = delay;
391
- if (this._isAutoBackgroundEnabled() && !remotePlayer._isPlaying) {
405
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning() && !remotePlayer._isPlaying) {
392
406
  this._startCountdown();
393
407
  }
394
408
  }
@@ -408,9 +422,24 @@ class Lifecycle extends LifecycleInterface {
408
422
  // Only start countdown if delay is positive
409
423
  if (timeoutDelay > 0) {
410
424
  sdkLogger.debug("Starting countdown timeout", timeoutDelay, isPlaying ? "(video)" : "(UI)");
411
- this._countdown = setTimeout(() => {
425
+ this._countdown = setTimeout(async () => {
412
426
  sdkLogger.debug("Countdown timeout reached, moving to background");
413
- this.moveToBackground();
427
+
428
+ // Create the event with cancelable: true
429
+ const event = new Event("beforestatechange", { cancelable: true });
430
+ event.state = this.UiState.BACKGROUND;
431
+ // Use the event manager to dispatch the event and wait for all listeners
432
+ await this._eventManager.dispatch("beforestatechange", event);
433
+ // Check if any listener called preventDefault()
434
+ if (event.defaultPrevented) {
435
+ sdkLogger.info("moveToBackground was prevented by a listener");
436
+ // Restart the countdown since the move was cancelled
437
+ if (this._isForegroundOrTransitioning()) {
438
+ this._startCountdown();
439
+ }
440
+ } else {
441
+ this.moveToBackground();
442
+ }
414
443
  }, timeoutDelay * 1000);
415
444
  } else {
416
445
  sdkLogger.debug("Countdown not started - delay is negative or zero");
@@ -424,8 +453,8 @@ class Lifecycle extends LifecycleInterface {
424
453
  if (this._countdown) {
425
454
  sdkLogger.debug("Stopping countdown timer");
426
455
  clearTimeout(this._countdown);
456
+ this._countdown = null;
427
457
  }
428
- this._countdown = null;
429
458
  }
430
459
 
431
460
  /**
@@ -459,7 +488,7 @@ class Lifecycle extends LifecycleInterface {
459
488
  moveToForeground() {
460
489
  if (window.cefQuery) {
461
490
  const inTransition = this._isInTransition();
462
- if (inTransition || this._state === this.UiState.FOREGROUND || this._state === this.UiState.IN_TRANSITION_TO_FOREGROUND) {
491
+ if (inTransition || this._isForegroundOrTransitioning()) {
463
492
  sdkLogger.warn(`lifecycle moveToForeground: No need to transition to foreground, state: ${this._state} transition: ${inTransition}`);
464
493
  return Promise.resolve(false);
465
494
  }
@@ -771,23 +800,19 @@ class Lifecycle extends LifecycleInterface {
771
800
  return Promise.reject("disconnect is not supported if NOT running e2e");
772
801
  }
773
802
 
803
+
774
804
  addEventListener(type, listener, options) {
775
- if (type === "userdisconnected") {
776
- // Use the event manager for userdisconnected events
805
+ if (Lifecycle._waitForListenersEvents.includes(type)) {
777
806
  this._eventManager.addEventListener(type, listener);
778
807
  } else {
779
- // For all other event types, use the parent class implementation
780
808
  super.addEventListener(type, listener, options);
781
809
  }
782
810
  }
783
811
 
784
-
785
812
  removeEventListener(type, listener, options) {
786
- if (type === "userdisconnected") {
787
- // Use the event manager for userdisconnected events
813
+ if (Lifecycle._waitForListenersEvents.includes(type)) {
788
814
  this._eventManager.removeEventListener(type, listener);
789
815
  } else {
790
- // For all other event types, use the parent class implementation
791
816
  super.removeEventListener(type, listener, options);
792
817
  }
793
818
  }
@@ -13,6 +13,23 @@ import {
13
13
  * });
14
14
  */
15
15
 
16
+ /**
17
+ * @event Lifecycle#beforestatechange
18
+ * @description Fired before transitioning to a new state. This event is cancelable.<br>
19
+ * Currently only fired when transitioning to BACKGROUND state (e.g., from autoBackground feature).<br>
20
+ * The actual state transition will occur after all event listeners have completed processing.
21
+ * Can be used to prevent automatic transitions to background state when using autoBackground feature.
22
+ * @property {UiState} state - Indicates the target state the lifecycle is trying to transition to.
23
+ * @property {boolean} cancelable - true, indicating the event can be cancelled using preventDefault()
24
+ * @example
25
+ * lifecycle.addEventListener("beforestatechange", (e) => {
26
+ * if (e.state === lifecycle.UiState.BACKGROUND && userIsInteracting) {
27
+ * // Prevent transition to background
28
+ * e.preventDefault();
29
+ * }
30
+ * });
31
+ */
32
+
16
33
  /**
17
34
  * @event Lifecycle#userinactivity
18
35
  * @description Fired after the ui has been inactive (i.e. no key presses) for a configurable number of seconds.<br>
@@ -265,7 +282,7 @@ class Lifecycle extends EventTarget {
265
282
  /**
266
283
  * Add event listener for lifecycle events
267
284
  * @param {string} type - The event type to listen for
268
- * @param {Function} listener - The callback function. Listeners for 'userdisconnected' events should return a promise to ensure the event is processed before the application exits.
285
+ * @param {Function} listener - The callback function. Listeners for 'userdisconnected' and 'beforestatechange' events should return a promise to ensure the event is processed before the application exits.
269
286
  * @param {Object} options - Event listener options
270
287
  */
271
288
  addEventListener(type, listener, options) {
@@ -1 +1 @@
1
- export const version = "4.4.4";
1
+ export const version = "4.4.5";