senza-sdk 4.2.47 → 4.2.49

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.2.47",
3
+ "version": "4.2.49",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
@@ -1,4 +1,5 @@
1
1
  import { getFCID, sdkLogger } from "./utils";
2
+ import { EventListenersManager } from "./eventListenersManager";
2
3
  import { lifecycle } from "./lifecycle";
3
4
 
4
5
  /**
@@ -25,71 +26,95 @@ class AlarmManager extends EventTarget {
25
26
  */
26
27
  constructor() {
27
28
  super();
28
- this._eventListeners = {};
29
- typeof document !== "undefined" && document.addEventListener("hs/alarmFiredEvent", (e) => {
30
- const logger = sdkLogger.withFields({alarmName: e.detail?.alarmName});
31
- logger.log("Got hs/alarmFiredEvent", JSON.stringify(e.detail));
29
+
30
+ /**
31
+ * Event listeners manager for alarm events
32
+ * @type {EventListenersManager}
33
+ * @private
34
+ */
35
+ this._eventManager = new EventListenersManager({
36
+ timeoutMs: 15000 // Default timeout of 15 seconds, can be overridden by _setDefaultTimeout
37
+ });
38
+
39
+ typeof document !== "undefined" && document.addEventListener("hs/alarmFiredEvent",async (e) => {
40
+
32
41
  if (e.detail?.alarmName) {
33
- if (this._eventListeners[e.detail.alarmName]) {
34
- const event = new CustomEvent(e.detail.alarmName, {detail: e.detail.payload});
35
- const timeBeforeAlarmHandling = Date.now();
36
- let sourceState;
37
- if (lifecycle.state === lifecycle.UiState.FOREGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND) {
38
- sourceState = "foreground";
39
- } else {
40
- sourceState = "background";
41
- }
42
- this._targetState = sourceState;
43
- Promise.allSettled(Array.from(this._eventListeners[e.detail.alarmName], cb => cb(event))).then(() => {
44
- const alarmHandlingDuration = Date.now() - timeBeforeAlarmHandling;
45
- logger.log(`All callbacks for alarm '${e.detail.alarmName}' are finished within ${alarmHandlingDuration}ms`);
46
- // If this alarm triggered the application, and moveToForeground has not been called as a result of the alarm
47
- // (i.e. the application is still in the background), we want to intentionally terminate the ui in order to save resources.
48
- const isTriggering = lifecycle.triggerEvent.type === "alarmFiredEvent" && lifecycle._triggerEventFcid && lifecycle._triggerEventFcid === e.detail.fcid;
49
- if (isTriggering) {
50
- if (!this._moveToForegroundHasBeenCalled && window.cefQuery) {
51
- logger.log("Application is about to be terminated since didn't move to foreground");
52
- const message = { type: "terminating" };
53
- const request = { target:"TC", waitForResponse: false, message: JSON.stringify(message) };
54
- window.cefQuery({
55
- request: JSON.stringify(request),
56
- persistent: false,
57
- onSuccess: () => {
58
- logger.log("terminating request successfully sent");
59
- },
60
- onFailure: (code, msg) => {
61
- logger.error(`terminating request failed: ${code} ${msg}`);
62
- }
63
- });
42
+ const logger = sdkLogger.withFields({alarmName: e.detail?.alarmName});
43
+ logger.log("Got hs/alarmFiredEvent", JSON.stringify(e.detail));
44
+
45
+ const timeBeforeAlarmHandling = Date.now();
46
+
47
+ let sourceState;
48
+ if (lifecycle.state === lifecycle.UiState.FOREGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND) {
49
+ sourceState = "foreground";
50
+ } else {
51
+ sourceState = "background";
52
+ }
53
+ this._targetState = sourceState;
54
+
55
+ // Create the custom event with payload
56
+ const event = new CustomEvent(e.detail.alarmName, {detail: e.detail.payload});
57
+
58
+ // Dispatch the event and wait for all listeners
59
+ await this._eventManager.dispatch(e.detail.alarmName, event);
60
+
61
+ const alarmHandlingDuration = Date.now() - timeBeforeAlarmHandling;
62
+ logger.log(`All callbacks for alarm '${e.detail.alarmName}' are finished within ${alarmHandlingDuration}ms`);
63
+ // If this alarm triggered the application, and moveToForeground has not been called as a result of the alarm
64
+ // (i.e. the application is still in the background), we want to intentionally terminate the ui in order to save resources.
65
+ const isTriggering = lifecycle.triggerEvent.type === "alarmFiredEvent" && lifecycle._triggerEventFcid && lifecycle._triggerEventFcid === e.detail.fcid;
66
+ if (isTriggering) {
67
+ if (!this._moveToForegroundHasBeenCalled && window.cefQuery) {
68
+ logger.log("Application is about to be terminated since didn't move to foreground");
69
+ const message = { type: "terminating" };
70
+ const request = { target:"TC", waitForResponse: false, message: JSON.stringify(message) };
71
+ window.cefQuery({
72
+ request: JSON.stringify(request),
73
+ persistent: false,
74
+ onSuccess: () => {
75
+ logger.log("terminating request successfully sent");
76
+ },
77
+ onFailure: (code, msg) => {
78
+ logger.error(`terminating request failed: ${code} ${msg}`);
64
79
  }
65
- }
66
- // For monitoring purposes, log a json with all the necessary labels and a unique prefix [hs-sdk][metrics]
67
- // The ui-streamer will recognize this prefix and handle the json accordingly.
68
- logger.metrics({
69
- type: "monitorAlarm",
70
- alarmName: e.detail.alarmName,
71
- isTriggering,
72
- sourceState,
73
- targetState: this._targetState,
74
- alarmHandlingDuration
75
80
  });
76
- });
81
+ }
77
82
  }
83
+ // For monitoring purposes, log a json with all the necessary labels and a unique prefix [hs-sdk][metrics]
84
+ // The ui-streamer will recognize this prefix and handle the json accordingly.
85
+ logger.metrics({
86
+ type: "monitorAlarm",
87
+ alarmName: e.detail.alarmName,
88
+ isTriggering,
89
+ sourceState,
90
+ targetState: this._targetState,
91
+ alarmHandlingDuration
92
+ });
78
93
  }
79
- });
94
+ }
95
+ );
80
96
  }
81
97
 
82
- addEventListener(type, callback) {
83
- if (!this._eventListeners[type]) {
84
- this._eventListeners[type] = new Set();
98
+ /**
99
+ * Set the default timeout for alarm event listeners
100
+ * @param {number} timeout - Timeout in milliseconds for alarm event listeners
101
+ * @private
102
+ */
103
+ _setDefaultTimeout(timeout) {
104
+ if (typeof timeout === "number" && timeout > 0) {
105
+ this._eventManager.timeoutMs = timeout;
106
+ sdkLogger.log(`Alarm event listener timeout set to ${timeout}ms`);
107
+ } else {
108
+ sdkLogger.warn(`Invalid timeout value: ${timeout}. Must be a positive number.`);
85
109
  }
86
- this._eventListeners[type].add(callback);
110
+ }
111
+
112
+ addEventListener(type, callback) {
113
+ this._eventManager.addEventListener(type, callback);
87
114
  }
88
115
 
89
116
  removeEventListener(type, callback){
90
- if (this._eventListeners[type]) {
91
- this._eventListeners[type].delete(callback);
92
- }
117
+ this._eventManager.removeEventListener(type, callback);
93
118
  }
94
119
 
95
120
  _moveToForegroundCalled() {
package/src/api.js CHANGED
@@ -4,6 +4,7 @@ import { sessionInfo } from "./SessionInfo";
4
4
  const { version } = pack;
5
5
  import { initSequence, showSequence } from "./devSequence.js";
6
6
  import { lifecycle } from "./lifecycle";
7
+ import { alarmManager } from "./alarmManager";
7
8
 
8
9
  let authToken;
9
10
 
@@ -110,6 +111,12 @@ export async function init() {
110
111
  sdkLogger.log(`onUpdateSessionEvent: token = ${authToken}`);
111
112
  });
112
113
 
114
+ // Set default alarm timeout using UI-Streamer settings
115
+ const alarmTimeout = sessionInfoObj?.settings?.["ui-streamer"]?.alarmTimeout;
116
+ if (alarmTimeout) {
117
+ alarmManager._setDefaultTimeout(alarmTimeout);
118
+ }
119
+
113
120
  // Get trigger event
114
121
  const triggerEventStr = await new Promise((resolve) => {
115
122
  const FCID = getFCID();
@@ -157,6 +164,8 @@ export async function init() {
157
164
  window.close = () => {
158
165
  sdkLogger.warn("window.close is disabled on Senza platform. Use lifecycle.exitApplication() instead.");
159
166
  };
167
+
168
+
160
169
  }
161
170
 
162
171
  /**
@@ -343,7 +352,7 @@ export function getDeviceInfo() {
343
352
  export { lifecycle } from "./lifecycle";
344
353
  export { deviceManager } from "./deviceManager";
345
354
  export { platformManager } from "./platformManager";
346
- export { alarmManager } from "./alarmManager";
355
+ export { alarmManager };
347
356
  export { messageManager } from "./messageManager";
348
357
  export { initSequence, showSequence } from "./devSequence";
349
358
  export { SenzaShakaPlayer as ShakaPlayer, shaka } from "./senzaShakaPlayer";
@@ -0,0 +1,108 @@
1
+ import { sdkLogger } from "./utils";
2
+
3
+ /**
4
+ * EventListenersManager - A utility class to manage event listeners with Promise-based completion tracking
5
+ * Handles both synchronous and asynchronous listeners, with timeout support
6
+ */
7
+ export class EventListenersManager {
8
+ /**
9
+ * Create a new EventListenersManager
10
+ * @param {Object} options - Configuration options
11
+ * @param {number} [options.timeoutMs=5000] - Timeout in milliseconds for each listener
12
+ */
13
+ constructor(options = {}) {
14
+ this.listeners = new Map(); // Map of event types to arrays of listeners
15
+ this.timeoutMs = options.timeoutMs || 5000;
16
+ }
17
+
18
+ /**
19
+ * Add an event listener for a specific event type
20
+ * @param {string} eventType - The type of event to listen for
21
+ * @param {Function} listener - The callback function
22
+ */
23
+ addEventListener(eventType, listener) {
24
+ if (!this.listeners.has(eventType)) {
25
+ this.listeners.set(eventType, []);
26
+ }
27
+ this.listeners.get(eventType).push(listener);
28
+ }
29
+
30
+ /**
31
+ * Remove an event listener for a specific event type
32
+ * @param {string} eventType - The type of event
33
+ * @param {Function} listener - The callback function to remove
34
+ */
35
+ removeEventListener(eventType, listener) {
36
+ if (!this.listeners.has(eventType)) return;
37
+
38
+ const listenerArray = this.listeners.get(eventType);
39
+ const index = listenerArray.indexOf(listener);
40
+ if (index !== -1) {
41
+ listenerArray.splice(index, 1);
42
+ }
43
+ }
44
+
45
+
46
+ /**
47
+ * Dispatch an event and wait for all listeners to complete
48
+ * @param {string} eventType - The type of event to dispatch
49
+ * @param {*} eventDetail - The event detail or Event object to pass to listeners
50
+ * @returns {Promise<void>} A promise that resolves when all listeners have completed
51
+ */
52
+ async dispatch(eventType, eventDetail = {}){
53
+ const eventListeners = this.listeners.get(eventType) || [];
54
+
55
+ if (eventListeners.length === 0) {
56
+ return;
57
+ }
58
+
59
+ // Create event object if eventDetail isn't already an Event
60
+ const event = eventDetail instanceof Event ? eventDetail : new CustomEvent(eventType, { detail: eventDetail });
61
+
62
+ // Execute all listeners and wrap each in a safe promise that won't reject
63
+ const listenerPromises = eventListeners.map(listener => {
64
+ try {
65
+ return Promise.resolve(listener(event))
66
+ .catch(error => {
67
+ sdkLogger.error(`Error in ${eventType} listener (promise rejection):`, error);
68
+ return undefined; // Convert rejected promises to resolved promises
69
+ });
70
+ } catch (error) {
71
+ sdkLogger.error(`Error in ${eventType} listener:`, error);
72
+ return Promise.resolve(); // Continue with other listeners even if one fails
73
+ }
74
+ });
75
+
76
+ try {
77
+ // Create a timeout timer we can cancel
78
+ let timeoutId = null;
79
+
80
+ // Create a single timeout promise for the entire batch
81
+ const timeoutPromise = new Promise((resolve) => {
82
+ timeoutId = setTimeout(() => {
83
+ sdkLogger.warn(`Event dispatch for ${eventType} timed out after ${this.timeoutMs}ms`);
84
+ resolve();
85
+ }, this.timeoutMs);
86
+ });
87
+
88
+ // Create a promise for all listeners completing
89
+ const allListenersPromise = Promise.allSettled(listenerPromises).then(results => {
90
+ // Clear the timeout when all listeners complete
91
+ if (timeoutId !== null) {
92
+ clearTimeout(timeoutId);
93
+ timeoutId = null;
94
+ }
95
+ return results;
96
+ });
97
+
98
+ // Race between all listeners completing and the timeout
99
+ await Promise.race([allListenersPromise, timeoutPromise]);
100
+
101
+
102
+ } catch (error) {
103
+ sdkLogger.error(`Error waiting for ${eventType} listeners to complete:`, error);
104
+
105
+ }
106
+
107
+ }
108
+ }
package/src/lifecycle.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  SenzaError,
10
10
  TargetPlayingState
11
11
  } from "./utils";
12
+ import { EventListenersManager } from "./eventListenersManager";
12
13
  import { sessionInfo } from "./SessionInfo";
13
14
  import { DEFAULT_REMOTE_PLAYER_CONFIRMATION_TIMEOUT, remotePlayer } from "./remotePlayer";
14
15
 
@@ -103,6 +104,15 @@ class Lifecycle extends EventTarget {
103
104
  this._isInitialized = false;
104
105
  this._inTransition = false;
105
106
 
107
+ /**
108
+ * Event listeners manager for the userdisconnected event
109
+ * @type {EventListenersManager}
110
+ * @private
111
+ */
112
+ this._eventManager = new EventListenersManager({
113
+ timeoutMs: 10000
114
+ });
115
+
106
116
  /**
107
117
  * @type {boolean}
108
118
  * @private
@@ -139,9 +149,14 @@ class Lifecycle extends EventTarget {
139
149
  this.dispatchEvent(event);
140
150
  });
141
151
 
142
- typeof document !== "undefined" && document.addEventListener("hs/endOfSession", () => {
152
+ typeof document !== "undefined" && document.addEventListener("hs/endOfSession", async () => {
153
+ // Create the event
143
154
  const event = new Event("userdisconnected");
144
- this.dispatchEvent(event);
155
+
156
+ // Use the event manager to dispatch the event and wait for all listeners
157
+ await this._eventManager.dispatch("userdisconnected", event);
158
+
159
+ this._sendTerminatingMessage();
145
160
  });
146
161
 
147
162
  typeof document !== "undefined" && document.addEventListener("keydown", () => {
@@ -167,9 +182,10 @@ class Lifecycle extends EventTarget {
167
182
  /** @private Initialize the lifecycle
168
183
  * @param {Object} uiStreamerSettings - UI-streamer portion of the settings taken from session info
169
184
  * @param {Object} [uiStreamerSettings.autoBackground] - Auto background mode configuration
170
- * @param {boolean} [uiStreamerSettings.autoBackground.enable=false] - Whether auto background mode is enabled
171
- * @param {number} [uiStreamerSettings.autoBackground.onVideoDelay=30] - Seconds of inactivity before moving to background while playing video
172
- * @param {number} [uiStreamerSettings.autoBackground.onUIDelay=30] - Seconds of inactivity before moving to background while in UI (not playing)
185
+ * @param {boolean} [uiStreamerSettings.autoBackground.enabled=false] - Enable/disable auto background
186
+ * @param {Object} [uiStreamerSettings.autoBackground.timeout] - Timeout settings
187
+ * @param {number|false} [uiStreamerSettings.autoBackground.timeout.playing=30] - Timeout in seconds when video is playing, false to disable
188
+ * @param {number|false} [uiStreamerSettings.autoBackground.timeout.idle=false] - Timeout in seconds when in UI mode, false to disable
173
189
  * @param {number} [uiStreamerSettings.remotePlayerConfirmationTimeout=5000] - Timeout in milliseconds for remote player operations
174
190
  * @param {number} [uiStreamerSettings.remotePlayerApiVersion=1] - Remote player API version
175
191
  */
@@ -246,23 +262,83 @@ class Lifecycle extends EventTarget {
246
262
  this._remotePlayerConfirmationTimeout = uiStreamerSettings?.remotePlayerConfirmationTimeout ?? DEFAULT_REMOTE_PLAYER_CONFIRMATION_TIMEOUT;
247
263
  this._remotePlayerApiVersion = uiStreamerSettings?.remotePlayerApiVersion || 1;
248
264
 
249
- // Take autoBackground settings from uiStreamerSettings if available
250
- // The UI setting section format :
251
- // "autoBackground": {
252
- // "enable": true,
253
- // "onVideoDelay": 30,
254
- // "onUIDelay": 30
255
- // }
256
-
257
- this._autoBackgroundOnVideoDelay = uiStreamerSettings?.autoBackground?.onVideoDelay ?? this._autoBackgroundOnVideoDelay;
258
- this._autoBackgroundOnUIDelay = uiStreamerSettings?.autoBackground?.onUIDelay ?? this._autoBackgroundOnUIDelay;
259
- this._autoBackground = uiStreamerSettings?.autoBackground?.enable ?? this._autoBackground;
260
- /*if (this._autoBackground) {
265
+ // Apply autoBackground settings if provided
266
+ if (uiStreamerSettings?.autoBackground) {
267
+ this.configure({
268
+ autoBackground: uiStreamerSettings.autoBackground
269
+ });
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Configure lifecycle settings
276
+ * @param {Object} config - Configuration object
277
+ * @param {Object} [config.autoBackground] - Auto background settings
278
+ * @param {boolean} [config.autoBackground.enabled] - Enable/disable auto background
279
+ * @param {Object} [config.autoBackground.timeout] - Timeout settings
280
+ * @param {number|false} [config.autoBackground.timeout.playing=30] - Timeout in seconds when video is playing, false to disable
281
+ * @param {number|false} [config.autoBackground.timeout.idle=false] - Timeout in seconds when in UI mode, false to disable
282
+ */
283
+ configure(config) {
284
+ if (config?.autoBackground) {
285
+ const { enabled, timeout } = config.autoBackground;
286
+
287
+ // Update enabled state
288
+ if (typeof enabled === "boolean") {
289
+ this._autoBackground = enabled;
290
+ }
291
+ // Update timeouts
292
+ if (timeout) {
293
+ if (timeout.playing !== undefined) {
294
+ if (timeout.playing === false) {
295
+ this._autoBackgroundOnVideoDelay = -1;
296
+ } else if (typeof timeout.playing === "number") {
297
+ this._autoBackgroundOnVideoDelay = timeout.playing;
298
+ } else {
299
+ sdkLogger.warn("Invalid autoBackground.timeout.playing value, expected number or false");
300
+ }
301
+ }
302
+ if (timeout.idle !== undefined) {
303
+ if (timeout.idle === false) {
304
+ this._autoBackgroundOnUIDelay = -1;
305
+ } else if (typeof timeout.idle === "number") {
306
+ this._autoBackgroundOnUIDelay = timeout.idle;
307
+ } else {
308
+ sdkLogger.warn("Invalid autoBackground.timeout.idle value, expected number or false");
309
+ }
310
+ }
311
+ }
312
+ // Start or stop countdown based on new configuration
313
+ if (this._autoBackground) {
261
314
  this._startCountdown();
262
- }*/
315
+ } else {
316
+ this._stopCountdown();
317
+ }
263
318
  }
264
319
  }
265
320
 
321
+ /**
322
+ * Get the current configuration settings
323
+ * @returns {Object} The current configuration object
324
+ * @example
325
+ * const config = lifecycle.getConfiguration();
326
+ * console.log(config.autoBackground.enabled); // true/false
327
+ * console.log(config.autoBackground.timeout.playing); // 30
328
+ * console.log(config.autoBackground.timeout.idle); // false
329
+ */
330
+ getConfiguration() {
331
+ return {
332
+ autoBackground: {
333
+ enabled: this._autoBackground,
334
+ timeout: {
335
+ playing: this._autoBackgroundOnVideoDelay <= 0 ? false : this._autoBackgroundOnVideoDelay,
336
+ idle: this._autoBackgroundOnUIDelay <= 0 ? false : this._autoBackgroundOnUIDelay
337
+ }
338
+ }
339
+ };
340
+ }
341
+
266
342
  /**
267
343
  * This method moves the application into standby mode, i.e. last ui frame is displayed and ui resources are released.
268
344
  * It should be called whenever the application wishes to go into standby mode and release resources.
@@ -396,10 +472,10 @@ class Lifecycle extends EventTarget {
396
472
  this._stopCountdown();
397
473
 
398
474
  const timeoutDelay = isPlaying ? this._autoBackgroundOnVideoDelay : this._autoBackgroundOnUIDelay;
399
- sdkLogger.debug("Starting countdown timeout", timeoutDelay, isPlaying ? "(video)" : "(UI)");
400
475
 
401
476
  // Only start countdown if delay is positive
402
477
  if (timeoutDelay > 0) {
478
+ sdkLogger.debug("Starting countdown timeout", timeoutDelay, isPlaying ? "(video)" : "(UI)");
403
479
  this._countdown = setTimeout(() => {
404
480
  sdkLogger.debug("Countdown timeout reached, moving to background");
405
481
  this.moveToBackground();
@@ -746,6 +822,64 @@ class Lifecycle extends EventTarget {
746
822
  sdkLogger.warn("exitApplication is not supported if NOT running e2e");
747
823
  return Promise.reject("exitApplication is not supported if NOT running e2e");
748
824
  }
825
+
826
+ /**
827
+ * Override addEventListener to handle "userdisconnected" event specially
828
+ * @param {string} type - The event type to listen for
829
+ * @param {Function} listener - The callback function
830
+ * @param {Object} options - Event listener options
831
+ */
832
+ addEventListener(type, listener, options) {
833
+ if (type === "userdisconnected") {
834
+ // Use the event manager for userdisconnected events
835
+ this._eventManager.addEventListener(type, listener);
836
+ } else {
837
+ // For all other event types, use the parent class implementation
838
+ super.addEventListener(type, listener, options);
839
+ }
840
+ }
841
+
842
+ /**
843
+ * Override removeEventListener to handle "userdisconnected" event specially
844
+ * @param {string} type - The event type
845
+ * @param {Function} listener - The callback function to remove
846
+ * @param {Object} options - Event listener options
847
+ */
848
+ removeEventListener(type, listener, options) {
849
+ if (type === "userdisconnected") {
850
+ // Use the event manager for userdisconnected events
851
+ this._eventManager.removeEventListener(type, listener);
852
+ } else {
853
+ // For all other event types, use the parent class implementation
854
+ super.removeEventListener(type, listener, options);
855
+ }
856
+ }
857
+
858
+ /**
859
+ * Sends the terminating message to the platform
860
+ * @private
861
+ */
862
+ _sendTerminatingMessage() {
863
+ if (window.cefQuery) {
864
+ const FCID = getFCID();
865
+ const message = {
866
+ type: "terminating",
867
+ fcid: FCID
868
+ };
869
+ const request = { target: "TC", waitForResponse: false, message: JSON.stringify(message) };
870
+
871
+ window.cefQuery({
872
+ request: JSON.stringify(request),
873
+ persistent: false,
874
+ onSuccess: () => {
875
+ sdkLogger.log("Terminating signal sent after userdisconnected event was processed");
876
+ },
877
+ onFailure: (code, msg) => {
878
+ sdkLogger.error(`Failed to send terminating signal: ${code} ${msg}`);
879
+ }
880
+ });
881
+ }
882
+ }
749
883
  }
750
884
 
751
885
  /**