senza-sdk 4.4.4 → 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.
@@ -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,8 +66,89 @@ 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
+ }
63
126
  }
64
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
+ }
144
+ }
145
+
146
+ // List of event types that should use the _eventManager
147
+ static _waitForListenersEvents = [
148
+ "userdisconnected","beforestatechange"
149
+ // Add more event types here if needed in the future
150
+ ];
151
+
65
152
  /**
66
153
  * @private Initialize the lifecycle
67
154
  * @param {Object} uiStreamerSettings - UI-streamer portion of the settings taken from session info
@@ -70,6 +157,11 @@ class Lifecycle extends LifecycleInterface {
70
157
  * @param {Object} [uiStreamerSettings.autoBackground.timeout] - Timeout settings
71
158
  * @param {number|false} [uiStreamerSettings.autoBackground.timeout.playing=30] - Timeout in seconds when video is playing, false to disable
72
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
73
165
  * @param {number} [uiStreamerSettings.remotePlayerConfirmationTimeout=5000] - Timeout in milliseconds for remote player operations
74
166
  * @param {number} [uiStreamerSettings.remotePlayerApiVersion=1] - Remote player API version
75
167
  */
@@ -165,6 +257,13 @@ class Lifecycle extends LifecycleInterface {
165
257
  autoBackground: uiStreamerSettings.autoBackground
166
258
  });
167
259
  }
260
+
261
+ // Apply autoSuspend settings if provided
262
+ if (uiStreamerSettings?.autoSuspend) {
263
+ this.configure({
264
+ autoSuspend: uiStreamerSettings.autoSuspend
265
+ });
266
+ }
168
267
  }
169
268
  }
170
269
 
@@ -177,9 +276,20 @@ class Lifecycle extends LifecycleInterface {
177
276
  const event = new Event("onstatechange");
178
277
  event.state = e.detail;
179
278
  this._state = event.state;
180
- if (this._isAutoBackgroundEnabled() && this.state === this.UiState.FOREGROUND) {
279
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning()) {
181
280
  this._startCountdown();
182
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
+ }
183
293
  this.dispatchEvent(event);
184
294
  });
185
295
 
@@ -201,9 +311,14 @@ class Lifecycle extends LifecycleInterface {
201
311
  });
202
312
 
203
313
  typeof document !== "undefined" && document.addEventListener("keydown", () => {
314
+ // Track the timestamp of the last user activity
315
+ this._lastActivityTimestamp = Date.now();
316
+
204
317
  if (this._isAutoBackgroundEnabled()) {
205
- if (this.state === this.UiState.BACKGROUND ||
206
- 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");
207
322
  this.moveToForeground();
208
323
  } else {
209
324
  this._startCountdown();
@@ -213,14 +328,27 @@ class Lifecycle extends LifecycleInterface {
213
328
 
214
329
  // Add playModeChange listener
215
330
  remotePlayer.addEventListener("playModeChange", (event) => {
216
- if (this._isAutoBackgroundEnabled() && this.state === this.UiState.FOREGROUND) {
331
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning()) {
217
332
  sdkLogger.log("Resetting auto background timer due to play mode change", event.detail.isPlaying);
218
333
  this._startCountdown(event.detail.isPlaying);
219
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
+ }
220
340
  });
221
341
  sdkLogger.log("[Lifecycle] Added event listeners for system events");
222
342
  }
223
343
 
344
+ /**
345
+ * @private Checks if the UI is in foreground or transitioning to foreground.
346
+ * @returns {boolean}
347
+ */
348
+ _isForegroundOrTransitioning() {
349
+ return this._state === this.UiState.FOREGROUND || this._state === this.UiState.IN_TRANSITION_TO_FOREGROUND;
350
+ }
351
+
224
352
  /**
225
353
  * @private Checks if auto background is enabled including overrides.
226
354
  * @returns {boolean}
@@ -253,6 +381,30 @@ class Lifecycle extends LifecycleInterface {
253
381
  return this._autoBackgroundOnUIDelay;
254
382
  }
255
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
+
256
408
  configure(config) {
257
409
  if (config?.autoBackground) {
258
410
  const { enabled, timeout } = config.autoBackground;
@@ -284,12 +436,45 @@ class Lifecycle extends LifecycleInterface {
284
436
  }
285
437
 
286
438
  // Start or stop countdown based on new configuration
287
- if (this._isAutoBackgroundEnabled()) {
439
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning()) {
288
440
  this._startCountdown();
289
441
  } else {
290
442
  this._stopCountdown();
291
443
  }
292
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
+ }
293
478
  }
294
479
 
295
480
  getConfiguration() {
@@ -300,6 +485,13 @@ class Lifecycle extends LifecycleInterface {
300
485
  playing: this._autoBackgroundOnVideoDelay <= 0 ? false : this._autoBackgroundOnVideoDelay,
301
486
  idle: this._autoBackgroundOnUIDelay <= 0 ? false : this._autoBackgroundOnUIDelay
302
487
  }
488
+ },
489
+ autoSuspend: {
490
+ enabled: this._autoSuspend.enabled,
491
+ timeout: {
492
+ playing: this._autoSuspend.timeout.playing,
493
+ idle: this._autoSuspend.timeout.idle
494
+ }
303
495
  }
304
496
  };
305
497
  }
@@ -364,7 +556,7 @@ class Lifecycle extends LifecycleInterface {
364
556
 
365
557
  set autoBackground(enabled) {
366
558
  this._autoBackground = enabled;
367
- if (this._isAutoBackgroundEnabled()) {
559
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning()) {
368
560
  this._startCountdown();
369
561
  } else {
370
562
  this._stopCountdown();
@@ -377,7 +569,7 @@ class Lifecycle extends LifecycleInterface {
377
569
 
378
570
  set autoBackgroundDelay(delay) {
379
571
  this._autoBackgroundOnVideoDelay = delay;
380
- if (this._isAutoBackgroundEnabled() && remotePlayer._isPlaying) {
572
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning() && remotePlayer._isPlaying) {
381
573
  this._startCountdown();
382
574
  }
383
575
  }
@@ -388,7 +580,7 @@ class Lifecycle extends LifecycleInterface {
388
580
 
389
581
  set autoBackgroundOnUIDelay(delay) {
390
582
  this._autoBackgroundOnUIDelay = delay;
391
- if (this._isAutoBackgroundEnabled() && !remotePlayer._isPlaying) {
583
+ if (this._isAutoBackgroundEnabled() && this._isForegroundOrTransitioning() && !remotePlayer._isPlaying) {
392
584
  this._startCountdown();
393
585
  }
394
586
  }
@@ -408,9 +600,25 @@ class Lifecycle extends LifecycleInterface {
408
600
  // Only start countdown if delay is positive
409
601
  if (timeoutDelay > 0) {
410
602
  sdkLogger.debug("Starting countdown timeout", timeoutDelay, isPlaying ? "(video)" : "(UI)");
411
- this._countdown = setTimeout(() => {
603
+ this._countdown = setTimeout(async () => {
412
604
  sdkLogger.debug("Countdown timeout reached, moving to background");
413
- this.moveToBackground();
605
+
606
+ // Create the event with cancelable: true
607
+ const event = new Event("beforestatechange", { cancelable: true });
608
+ event.state = this.UiState.BACKGROUND;
609
+ // Use the event manager to dispatch the event and wait for all listeners
610
+ await this._eventManager.dispatch("beforestatechange", event);
611
+ // Check if any listener called preventDefault()
612
+ if (event.defaultPrevented) {
613
+ sdkLogger.info("moveToBackground was prevented by a listener");
614
+ // Restart the countdown since the move was cancelled
615
+ if (this._isForegroundOrTransitioning()) {
616
+ this._startCountdown();
617
+ }
618
+ } else {
619
+ this.moveToBackground(true);
620
+ }
621
+
414
622
  }, timeoutDelay * 1000);
415
623
  } else {
416
624
  sdkLogger.debug("Countdown not started - delay is negative or zero");
@@ -424,8 +632,70 @@ class Lifecycle extends LifecycleInterface {
424
632
  if (this._countdown) {
425
633
  sdkLogger.debug("Stopping countdown timer");
426
634
  clearTimeout(this._countdown);
635
+ this._countdown = null;
636
+ }
637
+ }
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;
427
698
  }
428
- this._countdown = null;
429
699
  }
430
700
 
431
701
  /**
@@ -457,9 +727,15 @@ class Lifecycle extends LifecycleInterface {
457
727
  }
458
728
 
459
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
+
460
736
  if (window.cefQuery) {
461
737
  const inTransition = this._isInTransition();
462
- if (inTransition || this._state === this.UiState.FOREGROUND || this._state === this.UiState.IN_TRANSITION_TO_FOREGROUND) {
738
+ if (inTransition || this._isForegroundOrTransitioning()) {
463
739
  sdkLogger.warn(`lifecycle moveToForeground: No need to transition to foreground, state: ${this._state} transition: ${inTransition}`);
464
740
  return Promise.resolve(false);
465
741
  }
@@ -537,6 +813,16 @@ class Lifecycle extends LifecycleInterface {
537
813
  }
538
814
 
539
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
+ });
540
826
  if (window.cefQuery) {
541
827
  // If audio sync is disabled, we only need to sync before remote player starts playing
542
828
  if (!isAudioSyncConfigured()) {
@@ -600,6 +886,8 @@ class Lifecycle extends LifecycleInterface {
600
886
  const duration = Date.now() - timeBeforeSendingRequest;
601
887
  logger.withFields({ duration }).log(`play failed after ${duration} ms. Error code: ${code}, error message: ${msg}`);
602
888
  this._inTransitionToBackground = false;
889
+ // Clear the timer flag even if the transition fails
890
+ this._isBackgroundTriggeredByTimer = false;
603
891
  timerId = clearTimer(timerId);
604
892
  reject(new SenzaError(code, msg));
605
893
  }
@@ -610,6 +898,8 @@ class Lifecycle extends LifecycleInterface {
610
898
  timerId = setTimeout(() => {
611
899
  logger.log(`play reached timeout of ${timeout} ms, canceling query id ${queryId}`);
612
900
  this._inTransitionToBackground = false;
901
+ // Clear the timer flag even if the transition times out
902
+ this._isBackgroundTriggeredByTimer = false;
613
903
  window.cefQueryCancel(queryId);
614
904
  reject(new SenzaError(6000, `play reached timeout of ${timeout} ms`));
615
905
  }, timeout, queryId);
@@ -620,7 +910,11 @@ class Lifecycle extends LifecycleInterface {
620
910
  return Promise.resolve(false);
621
911
  }
622
912
 
623
- 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
+
624
918
  if (window.cefQuery) {
625
919
  const inTransition = this._isInTransition();
626
920
  if (inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
@@ -771,23 +1065,73 @@ class Lifecycle extends LifecycleInterface {
771
1065
  return Promise.reject("disconnect is not supported if NOT running e2e");
772
1066
  }
773
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
+
1122
+
774
1123
  addEventListener(type, listener, options) {
775
- if (type === "userdisconnected") {
776
- // Use the event manager for userdisconnected events
1124
+ if (Lifecycle._waitForListenersEvents.includes(type)) {
777
1125
  this._eventManager.addEventListener(type, listener);
778
1126
  } else {
779
- // For all other event types, use the parent class implementation
780
1127
  super.addEventListener(type, listener, options);
781
1128
  }
782
1129
  }
783
1130
 
784
-
785
1131
  removeEventListener(type, listener, options) {
786
- if (type === "userdisconnected") {
787
- // Use the event manager for userdisconnected events
1132
+ if (Lifecycle._waitForListenersEvents.includes(type)) {
788
1133
  this._eventManager.removeEventListener(type, listener);
789
1134
  } else {
790
- // For all other event types, use the parent class implementation
791
1135
  super.removeEventListener(type, listener, options);
792
1136
  }
793
1137
  }
@@ -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>
@@ -83,10 +100,15 @@ class Lifecycle extends EventTarget {
83
100
  * Configure lifecycle settings
84
101
  * @param {Object} config - Configuration object
85
102
  * @param {Object} [config.autoBackground] - Auto background settings
86
- * @param {boolean} [config.autoBackground.enabled] - Enable/disable auto background
103
+ * @param {boolean} [config.autoBackground.enabled=true] - Enable/disable auto background
87
104
  * @param {Object} [config.autoBackground.timeout] - Timeout settings
88
105
  * @param {number|false} [config.autoBackground.timeout.playing=30] - Timeout in seconds when video is playing, false to disable
89
- * @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
90
112
  */
91
113
  configure(config) {
92
114
  noop("lifecycle.configure", config);
@@ -97,9 +119,12 @@ class Lifecycle extends EventTarget {
97
119
  * @returns {Object} The current configuration object
98
120
  * @example
99
121
  * const config = lifecycle.getConfiguration();
100
- * console.log(config.autoBackground.enabled); // true/false
122
+ * console.log(config.autoBackground.enabled); // true
101
123
  * console.log(config.autoBackground.timeout.playing); // 30
102
- * 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
103
128
  */
104
129
  getConfiguration() {
105
130
  return {
@@ -109,6 +134,13 @@ class Lifecycle extends EventTarget {
109
134
  playing: 0,
110
135
  idle: 0
111
136
  }
137
+ },
138
+ autoSuspend: {
139
+ enabled: false,
140
+ timeout: {
141
+ playing: 0,
142
+ idle: 0
143
+ }
112
144
  }
113
145
  };
114
146
  }
@@ -262,10 +294,21 @@ class Lifecycle extends EventTarget {
262
294
  return noop("lifecycle.disconnect");
263
295
  }
264
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
+
265
308
  /**
266
309
  * Add event listener for lifecycle events
267
310
  * @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.
311
+ * @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
312
  * @param {Object} options - Event listener options
270
313
  */
271
314
  addEventListener(type, listener, options) {
@@ -1 +1 @@
1
- export const version = "4.4.4";
1
+ export const version = "4.4.6";