senza-sdk 4.2.47-a34cd6b.0 → 4.2.48

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-a34cd6b.0",
3
+ "version": "4.2.48",
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";
@@ -269,16 +269,18 @@ class DeviceManager extends EventTarget {
269
269
 
270
270
  /**
271
271
  * @typedef {Object} WiFiInfo
272
- * @property {string} ssid the name of the Wi-Fi network that the device is connected to
273
- * @property {string} bssid the unique identifier of the Wi-Fi access point
274
- * @property {string} standard the Wi-Fi standard in use, such as 802.11a/b/g/n/ac/ax
275
- * @property {string} security the type of security protocol used by the Wi-Fi network, such as WEP, WPA, WPA2, or WPA3
276
- * @property {string} device-mac the MAC address of the device
277
- * @property {string} device-ip4 the IPv4 address assigned to the device on the Wi-Fi network
278
- * @property {number} channel the number of the Wi-Fi channel currently being used
279
- * @property {number} width width of the Wi-Fi channel in megahertz, e.g. 20MHz or 40 MHz channel
280
- * @property {number} level a measure of the received signal strength, in the range 0 to 100 (the higher, the better). The level value is 100+RSSI (RSSI is the signal strength, measured in decibels)
281
- * @property {number} quality a measure of the signal quality, in the range 0 to 100 (the higher, the better). The quality value is derived from the signal EVM.
272
+ * @property {string} ssid the name of the Wi-Fi network that the device is connected to
273
+ * @property {string} bssid the unique identifier of the Wi-Fi access point
274
+ * @property {string} standard the Wi-Fi standard in use, such as 802.11a/b/g/n/ac/ax
275
+ * @property {string} security the type of security protocol used by the Wi-Fi network, such as WEP, WPA, WPA2, or WPA3
276
+ * @property {string} device-mac the MAC address of the device
277
+ * @property {string} device-ip4 the IPv4 address assigned to the device on the Wi-Fi network
278
+ * @property {string} dhcp-server the IP address of the DHCP server that assigned the device's network configuration
279
+ * @property {string[]} dns-server array of IP addresses of the DNS servers the device uses to resolve domain names
280
+ * @property {number} channel the number of the Wi-Fi channel currently being used
281
+ * @property {number} width width of the Wi-Fi channel in megahertz, e.g. 20MHz or 40 MHz channel
282
+ * @property {number} level a measure of the received signal strength, in the range 0 to 100 (the higher, the better). The level value is 100+RSSI (RSSI is the signal strength, measured in decibels)
283
+ * @property {number} quality a measure of the signal quality, in the range 0 to 100 (the higher, the better). The quality value is derived from the signal EVM.
282
284
  * */
283
285
 
284
286
  /**
@@ -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", () => {
@@ -746,6 +761,64 @@ class Lifecycle extends EventTarget {
746
761
  sdkLogger.warn("exitApplication is not supported if NOT running e2e");
747
762
  return Promise.reject("exitApplication is not supported if NOT running e2e");
748
763
  }
764
+
765
+ /**
766
+ * Override addEventListener to handle "userdisconnected" event specially
767
+ * @param {string} type - The event type to listen for
768
+ * @param {Function} listener - The callback function
769
+ * @param {Object} options - Event listener options
770
+ */
771
+ addEventListener(type, listener, options) {
772
+ if (type === "userdisconnected") {
773
+ // Use the event manager for userdisconnected events
774
+ this._eventManager.addEventListener(type, listener);
775
+ } else {
776
+ // For all other event types, use the parent class implementation
777
+ super.addEventListener(type, listener, options);
778
+ }
779
+ }
780
+
781
+ /**
782
+ * Override removeEventListener to handle "userdisconnected" event specially
783
+ * @param {string} type - The event type
784
+ * @param {Function} listener - The callback function to remove
785
+ * @param {Object} options - Event listener options
786
+ */
787
+ removeEventListener(type, listener, options) {
788
+ if (type === "userdisconnected") {
789
+ // Use the event manager for userdisconnected events
790
+ this._eventManager.removeEventListener(type, listener);
791
+ } else {
792
+ // For all other event types, use the parent class implementation
793
+ super.removeEventListener(type, listener, options);
794
+ }
795
+ }
796
+
797
+ /**
798
+ * Sends the terminating message to the platform
799
+ * @private
800
+ */
801
+ _sendTerminatingMessage() {
802
+ if (window.cefQuery) {
803
+ const FCID = getFCID();
804
+ const message = {
805
+ type: "terminating",
806
+ fcid: FCID
807
+ };
808
+ const request = { target: "TC", waitForResponse: false, message: JSON.stringify(message) };
809
+
810
+ window.cefQuery({
811
+ request: JSON.stringify(request),
812
+ persistent: false,
813
+ onSuccess: () => {
814
+ sdkLogger.log("Terminating signal sent after userdisconnected event was processed");
815
+ },
816
+ onFailure: (code, msg) => {
817
+ sdkLogger.error(`Failed to send terminating signal: ${code} ${msg}`);
818
+ }
819
+ });
820
+ }
821
+ }
749
822
  }
750
823
 
751
824
  /**
@@ -199,7 +199,8 @@ class RemotePlayer extends EventTarget {
199
199
  sdkLogger.info("Got hs/playbackInfoEvent");
200
200
  // When attached, the sdk controls the synchronization between the local and remote player.
201
201
  if (this._videoElement) {
202
- if ((lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
202
+ // When audio sync is configured, the local player is already in sync with the remote player.
203
+ if (!isAudioSyncConfigured() && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
203
204
  this._syncLocalPlayerWithRemotePlayer();
204
205
  }
205
206
  } else {
@@ -621,7 +622,6 @@ class RemotePlayer extends EventTarget {
621
622
 
622
623
  _syncLocalPlayerWithRemotePlayer() {
623
624
  if (this._videoElement) {
624
- if (isAudioSyncConfigured()) this._isSeekingByPlatform = true;
625
625
  this._videoElement.currentTime = this.currentTime;
626
626
  }
627
627
  }