senza-sdk 4.4.5 → 4.4.6

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.5",
3
+ "version": "4.4.6",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
@@ -17,9 +17,15 @@ import { DEFAULT_REMOTE_PLAYER_CONFIRMATION_TIMEOUT, remotePlayer } from "./remo
17
17
  import {bus, Events} from "./eventBus";
18
18
 
19
19
  // Default values for autoBackground settings. These values are used if the UIStreamer settings are not provided.
20
- const DEFAULT_AUTO_BACKGROUND_VIDEO_DELAY = 30;
21
- const DEFAULT_AUTO_BACKGROUND_UI_DELAY = -1;
22
- const DEFAULT_AUTO_BACKGROUND_ENABLED = false;
20
+ const DEFAULT_AUTO_BACKGROUND_VIDEO_DELAY = 10*60;
21
+ const DEFAULT_AUTO_BACKGROUND_UI_DELAY = 10*60;
22
+ const DEFAULT_AUTO_BACKGROUND_ENABLED = true;
23
+ // Default values for autoSuspend settings
24
+ const DEFAULT_AUTO_SUSPEND_ENABLED = false;
25
+ const DEFAULT_AUTO_SUSPEND_PLAYING_DELAY = 60;
26
+ const DEFAULT_AUTO_SUSPEND_IDLE_DELAY = 60;
27
+ // Session storage key for tracking timer-triggered background state
28
+ const BACKGROUND_TIMER_KEY = "senzaSDK_backgroundTriggeredByTimer";
23
29
 
24
30
  class Lifecycle extends LifecycleInterface {
25
31
  constructor() {
@@ -60,6 +66,81 @@ class Lifecycle extends LifecycleInterface {
60
66
  * @private
61
67
  */
62
68
  this._autoBackgroundOnUIDelay = DEFAULT_AUTO_BACKGROUND_UI_DELAY;
69
+
70
+ /**
71
+ * Timestamp of the last user activity
72
+ * @type {number}
73
+ * @private
74
+ */
75
+ this._lastActivityTimestamp = Date.now();
76
+
77
+ /**
78
+ * Auto suspend configuration
79
+ * @type {Object}
80
+ * @private
81
+ */
82
+ this._autoSuspend = {
83
+ enabled: DEFAULT_AUTO_SUSPEND_ENABLED,
84
+ timeout: {
85
+ playing: DEFAULT_AUTO_SUSPEND_PLAYING_DELAY,
86
+ idle: DEFAULT_AUTO_SUSPEND_IDLE_DELAY
87
+ }
88
+ };
89
+
90
+ /**
91
+ * Timer for auto suspend functionality
92
+ * @type {number|null}
93
+ * @private
94
+ */
95
+ this._suspendCountdown = null;
96
+
97
+ /**
98
+ * Timestamp when the application entered background state
99
+ * @type {number}
100
+ * @private
101
+ */
102
+ this._backgroundTimestamp = Date.now();
103
+
104
+ /**
105
+ * Flag to track if suspend was triggered by timer
106
+ * @type {boolean}
107
+ * @private
108
+ */
109
+ this._isSuspendTriggeredByTimer = false;
110
+
111
+ }
112
+
113
+ /**
114
+ * @private Gets the background triggered by timer flag from sessionStorage
115
+ * @returns {boolean}
116
+ */
117
+ get _isBackgroundTriggeredByTimer() {
118
+ try {
119
+ const value = typeof window !== "undefined" && window.sessionStorage ?
120
+ window.sessionStorage.getItem(BACKGROUND_TIMER_KEY) : null;
121
+ return value === "true";
122
+ } catch (error) {
123
+ sdkLogger.warn("Failed to read _isBackgroundTriggeredByTimer from sessionStorage:", error);
124
+ return false;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * @private Sets the background triggered by timer flag in sessionStorage
130
+ * @param {boolean} value
131
+ */
132
+ set _isBackgroundTriggeredByTimer(value) {
133
+ try {
134
+ if (typeof window !== "undefined" && window.sessionStorage) {
135
+ if (value) {
136
+ window.sessionStorage.setItem(BACKGROUND_TIMER_KEY, "true");
137
+ } else {
138
+ window.sessionStorage.removeItem(BACKGROUND_TIMER_KEY);
139
+ }
140
+ }
141
+ } catch (error) {
142
+ sdkLogger.warn("Failed to write _backgroundTriggeredByTimer to sessionStorage:", error);
143
+ }
63
144
  }
64
145
 
65
146
  // List of event types that should use the _eventManager
@@ -76,6 +157,11 @@ class Lifecycle extends LifecycleInterface {
76
157
  * @param {Object} [uiStreamerSettings.autoBackground.timeout] - Timeout settings
77
158
  * @param {number|false} [uiStreamerSettings.autoBackground.timeout.playing=30] - Timeout in seconds when video is playing, false to disable
78
159
  * @param {number|false} [uiStreamerSettings.autoBackground.timeout.idle=false] - Timeout in seconds when in UI mode, false to disable
160
+ * @param {Object} [uiStreamerSettings.autoSuspend] - Auto suspend mode configuration
161
+ * @param {boolean} [uiStreamerSettings.autoSuspend.enabled=false] - Enable/disable auto suspend
162
+ * @param {Object} [uiStreamerSettings.autoSuspend.timeout] - Timeout settings
163
+ * @param {number} [uiStreamerSettings.autoSuspend.timeout.playing=60] - Timeout in seconds when video is playing
164
+ * @param {number} [uiStreamerSettings.autoSuspend.timeout.idle=60] - Timeout in seconds when in idle mode
79
165
  * @param {number} [uiStreamerSettings.remotePlayerConfirmationTimeout=5000] - Timeout in milliseconds for remote player operations
80
166
  * @param {number} [uiStreamerSettings.remotePlayerApiVersion=1] - Remote player API version
81
167
  */
@@ -171,6 +257,13 @@ class Lifecycle extends LifecycleInterface {
171
257
  autoBackground: uiStreamerSettings.autoBackground
172
258
  });
173
259
  }
260
+
261
+ // Apply autoSuspend settings if provided
262
+ if (uiStreamerSettings?.autoSuspend) {
263
+ this.configure({
264
+ autoSuspend: uiStreamerSettings.autoSuspend
265
+ });
266
+ }
174
267
  }
175
268
  }
176
269
 
@@ -186,6 +279,17 @@ class Lifecycle extends LifecycleInterface {
186
279
  if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning()) {
187
280
  this._startCountdown();
188
281
  }
282
+ // Track timestamp when entering background state
283
+ if (this._state === this.UiState.BACKGROUND) {
284
+ this._backgroundTimestamp = Date.now();
285
+ }
286
+ // Start suspend countdown when entering background state
287
+ if (this._isAutoSuspendEnabled() && this._state === this.UiState.BACKGROUND) {
288
+ this._startSuspendCountdown();
289
+ } else {
290
+ // Stop suspend countdown when leaving background state
291
+ this._stopSuspendCountdown();
292
+ }
189
293
  this.dispatchEvent(event);
190
294
  });
191
295
 
@@ -207,9 +311,14 @@ class Lifecycle extends LifecycleInterface {
207
311
  });
208
312
 
209
313
  typeof document !== "undefined" && document.addEventListener("keydown", () => {
314
+ // Track the timestamp of the last user activity
315
+ this._lastActivityTimestamp = Date.now();
316
+
210
317
  if (this._isAutoBackgroundEnabled()) {
211
- if (this.state === this.UiState.BACKGROUND ||
212
- this.state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
318
+
319
+ if (this._isBackgroundTriggeredByTimer && (this.state === this.UiState.BACKGROUND ||
320
+ this.state === this.UiState.IN_TRANSITION_TO_BACKGROUND)) {
321
+ sdkLogger.log("Moving to foreground due to user interaction after auto background");
213
322
  this.moveToForeground();
214
323
  } else {
215
324
  this._startCountdown();
@@ -223,6 +332,11 @@ class Lifecycle extends LifecycleInterface {
223
332
  sdkLogger.log("Resetting auto background timer due to play mode change", event.detail.isPlaying);
224
333
  this._startCountdown(event.detail.isPlaying);
225
334
  }
335
+ // Also restart suspend countdown if in background state
336
+ if (this._isAutoSuspendEnabled() && this._state === this.UiState.BACKGROUND) {
337
+ sdkLogger.log("Resetting auto suspend timer due to play mode change", event.detail.isPlaying);
338
+ this._startSuspendCountdown(event.detail.isPlaying);
339
+ }
226
340
  });
227
341
  sdkLogger.log("[Lifecycle] Added event listeners for system events");
228
342
  }
@@ -267,6 +381,30 @@ class Lifecycle extends LifecycleInterface {
267
381
  return this._autoBackgroundOnUIDelay;
268
382
  }
269
383
 
384
+ /**
385
+ * @private Checks if auto suspend is enabled.
386
+ * @returns {boolean}
387
+ */
388
+ _isAutoSuspendEnabled() {
389
+ return this._autoSuspend.enabled;
390
+ }
391
+
392
+ /**
393
+ * @private Gets the auto suspend playing delay.
394
+ * @returns {number}
395
+ */
396
+ _getAutoSuspendOnPlayingDelay() {
397
+ return this._autoSuspend.timeout.playing;
398
+ }
399
+
400
+ /**
401
+ * @private Gets the auto suspend idle delay.
402
+ * @returns {number}
403
+ */
404
+ _getAutoSuspendOnIdleDelay() {
405
+ return this._autoSuspend.timeout.idle;
406
+ }
407
+
270
408
  configure(config) {
271
409
  if (config?.autoBackground) {
272
410
  const { enabled, timeout } = config.autoBackground;
@@ -304,6 +442,39 @@ class Lifecycle extends LifecycleInterface {
304
442
  this._stopCountdown();
305
443
  }
306
444
  }
445
+
446
+ if (config?.autoSuspend) {
447
+ const { enabled, timeout } = config.autoSuspend;
448
+
449
+ // Update enabled state
450
+ if (typeof enabled === "boolean") {
451
+ this._autoSuspend.enabled = enabled;
452
+ }
453
+ // Update timeouts
454
+ if (timeout) {
455
+ if (timeout.playing !== undefined) {
456
+ if (typeof timeout.playing === "number") {
457
+ this._autoSuspend.timeout.playing = timeout.playing;
458
+ } else {
459
+ sdkLogger.warn("Invalid autoSuspend.timeout.playing value, expected number");
460
+ }
461
+ }
462
+ if (timeout.idle !== undefined) {
463
+ if (typeof timeout.idle === "number") {
464
+ this._autoSuspend.timeout.idle = timeout.idle;
465
+ } else {
466
+ sdkLogger.warn("Invalid autoSuspend.timeout.idle value, expected number");
467
+ }
468
+ }
469
+ }
470
+
471
+ // Start or stop suspend countdown based on new configuration
472
+ if (this._isAutoSuspendEnabled() && this._state === this.UiState.BACKGROUND) {
473
+ this._startSuspendCountdown();
474
+ } else {
475
+ this._stopSuspendCountdown();
476
+ }
477
+ }
307
478
  }
308
479
 
309
480
  getConfiguration() {
@@ -314,6 +485,13 @@ class Lifecycle extends LifecycleInterface {
314
485
  playing: this._autoBackgroundOnVideoDelay <= 0 ? false : this._autoBackgroundOnVideoDelay,
315
486
  idle: this._autoBackgroundOnUIDelay <= 0 ? false : this._autoBackgroundOnUIDelay
316
487
  }
488
+ },
489
+ autoSuspend: {
490
+ enabled: this._autoSuspend.enabled,
491
+ timeout: {
492
+ playing: this._autoSuspend.timeout.playing,
493
+ idle: this._autoSuspend.timeout.idle
494
+ }
317
495
  }
318
496
  };
319
497
  }
@@ -438,8 +616,9 @@ class Lifecycle extends LifecycleInterface {
438
616
  this._startCountdown();
439
617
  }
440
618
  } else {
441
- this.moveToBackground();
619
+ this.moveToBackground(true);
442
620
  }
621
+
443
622
  }, timeoutDelay * 1000);
444
623
  } else {
445
624
  sdkLogger.debug("Countdown not started - delay is negative or zero");
@@ -457,6 +636,68 @@ class Lifecycle extends LifecycleInterface {
457
636
  }
458
637
  }
459
638
 
639
+ /**
640
+ * @private Start the suspend countdown timer when in background state
641
+ */
642
+ _startSuspendCountdown(isPlaying = remotePlayer._isPlaying) {
643
+ this._stopSuspendCountdown();
644
+
645
+ // Only start if we're in background state and auto suspend is enabled
646
+ if (this._state !== this.UiState.BACKGROUND || !this._isAutoSuspendEnabled()) {
647
+ return;
648
+ }
649
+
650
+ const timeoutDelay = isPlaying ? this._getAutoSuspendOnPlayingDelay() : this._getAutoSuspendOnIdleDelay();
651
+
652
+ // Only start countdown if delay is positive
653
+ if (timeoutDelay > 0) {
654
+ sdkLogger.debug("Starting suspend countdown timeout", timeoutDelay, isPlaying ? "(playing)" : "(idle)");
655
+ this._suspendCountdown = setTimeout(async () => {
656
+ sdkLogger.debug("Suspend countdown timeout reached, moving to suspended");
657
+ if (this._state === this.UiState.BACKGROUND) {
658
+ // Create the event with cancelable: true
659
+ const event = new Event("beforestatechange", { cancelable: true });
660
+ event.state = this.UiState.SUSPENDED;
661
+ // Use the event manager to dispatch the event and wait for all listeners
662
+ await this._eventManager.dispatch("beforestatechange", event);
663
+ // Check if any listener called preventDefault()
664
+ if (event.defaultPrevented) {
665
+ sdkLogger.info("moveToSuspended was prevented by a listener");
666
+ // Restart the suspend countdown since the move was cancelled
667
+ this._startSuspendCountdown();
668
+ } else {
669
+ try {
670
+ // Set flag to indicate suspend was triggered by timer
671
+ this._isSuspendTriggeredByTimer = true;
672
+ await this.moveToSuspended();
673
+ } catch (error) {
674
+ sdkLogger.error("Failed to move to suspended:", error);
675
+ }
676
+ }
677
+
678
+ } else {
679
+ sdkLogger.warn("suspend timer was called while not in background");
680
+ }
681
+
682
+ }, timeoutDelay * 1000);
683
+ } else {
684
+ sdkLogger.debug("Suspend countdown not started - delay is zero or negative");
685
+ }
686
+ }
687
+
688
+ /**
689
+ * @private Stop the suspend countdown timer
690
+ */
691
+ _stopSuspendCountdown() {
692
+ if (this._suspendCountdown) {
693
+ sdkLogger.debug("Stopping suspend countdown timer");
694
+ clearTimeout(this._suspendCountdown);
695
+ this._suspendCountdown = null;
696
+ // Reset the timer flag when stopping countdown
697
+ this._isSuspendTriggeredByTimer = false;
698
+ }
699
+ }
700
+
460
701
  /**
461
702
  * @private
462
703
  */
@@ -486,6 +727,12 @@ class Lifecycle extends LifecycleInterface {
486
727
  }
487
728
 
488
729
  moveToForeground() {
730
+ // Reset activity timestamp when moving to foreground
731
+ this._lastActivityTimestamp = Date.now();
732
+
733
+ // Clear the timer-triggered background flag when moving to foreground
734
+ this._isBackgroundTriggeredByTimer = false;
735
+
489
736
  if (window.cefQuery) {
490
737
  const inTransition = this._isInTransition();
491
738
  if (inTransition || this._isForegroundOrTransitioning()) {
@@ -566,6 +813,16 @@ class Lifecycle extends LifecycleInterface {
566
813
  }
567
814
 
568
815
  _moveToBackground() {
816
+ // Report metrics for time since last user activity when moving to background
817
+
818
+ const duration = (Date.now() - this._lastActivityTimestamp)/1000;
819
+
820
+ sdkLogger.metrics({
821
+ type: "backgroundTransitions",
822
+ isAutoBackground: this._isBackgroundTriggeredByTimer,
823
+ isPlaying: remotePlayer._isPlaying,
824
+ duration
825
+ });
569
826
  if (window.cefQuery) {
570
827
  // If audio sync is disabled, we only need to sync before remote player starts playing
571
828
  if (!isAudioSyncConfigured()) {
@@ -629,6 +886,8 @@ class Lifecycle extends LifecycleInterface {
629
886
  const duration = Date.now() - timeBeforeSendingRequest;
630
887
  logger.withFields({ duration }).log(`play failed after ${duration} ms. Error code: ${code}, error message: ${msg}`);
631
888
  this._inTransitionToBackground = false;
889
+ // Clear the timer flag even if the transition fails
890
+ this._isBackgroundTriggeredByTimer = false;
632
891
  timerId = clearTimer(timerId);
633
892
  reject(new SenzaError(code, msg));
634
893
  }
@@ -639,6 +898,8 @@ class Lifecycle extends LifecycleInterface {
639
898
  timerId = setTimeout(() => {
640
899
  logger.log(`play reached timeout of ${timeout} ms, canceling query id ${queryId}`);
641
900
  this._inTransitionToBackground = false;
901
+ // Clear the timer flag even if the transition times out
902
+ this._isBackgroundTriggeredByTimer = false;
642
903
  window.cefQueryCancel(queryId);
643
904
  reject(new SenzaError(6000, `play reached timeout of ${timeout} ms`));
644
905
  }, timeout, queryId);
@@ -649,7 +910,11 @@ class Lifecycle extends LifecycleInterface {
649
910
  return Promise.resolve(false);
650
911
  }
651
912
 
652
- moveToBackground() {
913
+ moveToBackground(isTriggeredByTimer = false) {
914
+
915
+ // If the background transition is triggered by the auto background timer, set the flag
916
+ this._isBackgroundTriggeredByTimer = isTriggeredByTimer;
917
+
653
918
  if (window.cefQuery) {
654
919
  const inTransition = this._isInTransition();
655
920
  if (inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
@@ -800,6 +1065,60 @@ class Lifecycle extends LifecycleInterface {
800
1065
  return Promise.reject("disconnect is not supported if NOT running e2e");
801
1066
  }
802
1067
 
1068
+ moveToSuspended() {
1069
+ // Check if the current state is BACKGROUND
1070
+ if (this._state !== this.UiState.BACKGROUND) {
1071
+ const errorMsg = `moveToSuspended can only be called from BACKGROUND state. Current state: ${this._state}`;
1072
+ sdkLogger.error(errorMsg);
1073
+ return Promise.reject(errorMsg);
1074
+ }
1075
+
1076
+ // Report metrics for time between background and suspend
1077
+ const duration = (Date.now() - this._backgroundTimestamp) / 1000;
1078
+
1079
+ sdkLogger.metrics({
1080
+ type: "suspendTransitions",
1081
+ isAutoSuspend: this._isSuspendTriggeredByTimer,
1082
+ isPlaying: remotePlayer._isPlaying,
1083
+ duration
1084
+ });
1085
+
1086
+ // Reset the timer flag after reporting metrics
1087
+ const reason = this._isSuspendTriggeredByTimer ? "timer" : "appInitiated";
1088
+ this._isSuspendTriggeredByTimer = false;
1089
+
1090
+ if (window.cefQuery) {
1091
+ return new Promise((resolve, reject) => {
1092
+ const FCID = getFCID();
1093
+ const logger = sdkLogger.withFields({ FCID });
1094
+ const message = {
1095
+ type: "suspend",
1096
+ reason,
1097
+ fcid: FCID
1098
+ };
1099
+ const request = { target: "TC",
1100
+ waitForResponse: false,
1101
+ message: JSON.stringify(message) };
1102
+
1103
+ logger.log("moveToSuspended: sending suspend message");
1104
+ window.cefQuery({
1105
+ request: JSON.stringify(request),
1106
+ persistent: false,
1107
+ onSuccess: () => {
1108
+ logger.log("moveToSuspended request successfully sent");
1109
+ resolve(true);
1110
+ },
1111
+ onFailure: (code, msg) => {
1112
+ logger.error(`moveToSuspended failed: ${code} ${msg}`);
1113
+ reject(new SenzaError(code, msg));
1114
+ }
1115
+ });
1116
+ });
1117
+ }
1118
+ sdkLogger.warn("moveToSuspended is not supported if NOT running e2e");
1119
+ return Promise.reject("moveToSuspended is not supported if NOT running e2e");
1120
+ }
1121
+
803
1122
 
804
1123
  addEventListener(type, listener, options) {
805
1124
  if (Lifecycle._waitForListenersEvents.includes(type)) {
@@ -100,10 +100,15 @@ class Lifecycle extends EventTarget {
100
100
  * Configure lifecycle settings
101
101
  * @param {Object} config - Configuration object
102
102
  * @param {Object} [config.autoBackground] - Auto background settings
103
- * @param {boolean} [config.autoBackground.enabled] - Enable/disable auto background
103
+ * @param {boolean} [config.autoBackground.enabled=true] - Enable/disable auto background
104
104
  * @param {Object} [config.autoBackground.timeout] - Timeout settings
105
105
  * @param {number|false} [config.autoBackground.timeout.playing=30] - Timeout in seconds when video is playing, false to disable
106
- * @param {number|false} [config.autoBackground.timeout.idle=false] - Timeout in seconds when in UI mode, false to disable
106
+ * @param {number|false} [config.autoBackground.timeout.idle=30] - Timeout in seconds when in UI mode, false to disable
107
+ * @param {Object} [config.autoSuspend] - Auto suspend settings
108
+ * @param {boolean} [config.autoSuspend.enabled=false] - Enable/disable auto suspend
109
+ * @param {Object} [config.autoSuspend.timeout] - Timeout settings
110
+ * @param {number} [config.autoSuspend.timeout.playing=60] - Timeout in seconds when video is playing before moving to suspended state
111
+ * @param {number} [config.autoSuspend.timeout.idle=60] - Timeout in seconds when in background mode before moving to suspended state
107
112
  */
108
113
  configure(config) {
109
114
  noop("lifecycle.configure", config);
@@ -114,9 +119,12 @@ class Lifecycle extends EventTarget {
114
119
  * @returns {Object} The current configuration object
115
120
  * @example
116
121
  * const config = lifecycle.getConfiguration();
117
- * console.log(config.autoBackground.enabled); // true/false
122
+ * console.log(config.autoBackground.enabled); // true
118
123
  * console.log(config.autoBackground.timeout.playing); // 30
119
- * console.log(config.autoBackground.timeout.idle); // false
124
+ * console.log(config.autoBackground.timeout.idle); // 30
125
+ * console.log(config.autoSuspend.enabled); // false
126
+ * console.log(config.autoSuspend.timeout.playing); // 60
127
+ * console.log(config.autoSuspend.timeout.idle); // 60
120
128
  */
121
129
  getConfiguration() {
122
130
  return {
@@ -126,6 +134,13 @@ class Lifecycle extends EventTarget {
126
134
  playing: 0,
127
135
  idle: 0
128
136
  }
137
+ },
138
+ autoSuspend: {
139
+ enabled: false,
140
+ timeout: {
141
+ playing: 0,
142
+ idle: 0
143
+ }
129
144
  }
130
145
  };
131
146
  }
@@ -279,6 +294,17 @@ class Lifecycle extends EventTarget {
279
294
  return noop("lifecycle.disconnect");
280
295
  }
281
296
 
297
+ /**
298
+ * Use this api to move the application to suspended state.
299
+ * This method can only be called when the application is in BACKGROUND state.
300
+ * @return {Promise} Promise which is resolved when the suspend command has been successfully sent.
301
+ * Failure to process the suspend command will result in the promise being rejected.
302
+ * @alpha API has not yet been released
303
+ */
304
+ moveToSuspended() {
305
+ return noop("lifecycle.moveToSuspended");
306
+ }
307
+
282
308
  /**
283
309
  * Add event listener for lifecycle events
284
310
  * @param {string} type - The event type to listen for
@@ -1 +1 @@
1
- export const version = "4.4.5";
1
+ export const version = "4.4.6";