senza-sdk 4.5.0 → 4.5.1

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.5.0",
3
+ "version": "4.5.1",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
@@ -757,6 +757,7 @@ class Lifecycle extends LifecycleInterface {
757
757
  class: "remotePlayer",
758
758
  action: "stop",
759
759
  streamType: remotePlayer._isAudioSyncEnabled() ? StreamType.VIDEO | StreamType.SUBTITLE : StreamType.AUDIO | StreamType.VIDEO | StreamType.SUBTITLE,
760
+ switchMode: remotePlayer._isAudioSyncEnabled() ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS,
760
761
  fcid: FCID
761
762
  };
762
763
  let timerId = 0;
@@ -7,15 +7,49 @@ class Overlay extends OverlayInterface {
7
7
 
8
8
  this._element = null;
9
9
  this._configuration = {
10
- useTransparency: true
10
+ useTransparency: true,
11
+ autoRefreshSeconds: 2
11
12
  };
13
+ this._retryGeneration = 0;
14
+ this._retryTimerId = null;
12
15
  }
13
16
 
14
17
  _isTransparencyEnabled() {
15
18
  return this._configuration.useTransparency !== false;
16
19
  }
17
20
 
18
- _sendFrame(x, y, width, height) {
21
+ _clearRetryTimers() {
22
+ this._retryGeneration++;
23
+ clearTimeout(this._retryTimerId);
24
+ this._retryTimerId = null;
25
+ }
26
+
27
+ _scheduleRetries(maxMs) {
28
+ const startTime = Date.now();
29
+ let delay = 100;
30
+ const generation = ++this._retryGeneration;
31
+
32
+ const scheduleNext = () => {
33
+ if (this._retryGeneration !== generation) return;
34
+ const elapsed = Date.now() - startTime;
35
+ if (elapsed + delay > maxMs) return;
36
+
37
+ this._retryTimerId = setTimeout(async () => {
38
+ if (this._retryGeneration !== generation) return;
39
+ try {
40
+ await this._renderFrame(true);
41
+ } catch (err) {
42
+ sdkLogger.log(`Overlay: conditional refresh error: ${err}`);
43
+ }
44
+ delay *= 2;
45
+ scheduleNext();
46
+ }, delay);
47
+ };
48
+
49
+ scheduleNext();
50
+ }
51
+
52
+ _sendFrame(x, y, width, height, conditional = false) {
19
53
  if (window.cefQuery) {
20
54
  return new Promise((resolve, reject) => {
21
55
  const FCID = getFCID();
@@ -26,7 +60,8 @@ class Overlay extends OverlayInterface {
26
60
  width,
27
61
  height,
28
62
  useTransparency: this._isTransparencyEnabled(),
29
- fcid: FCID
63
+ fcid: FCID,
64
+ conditional
30
65
  };
31
66
  const request = { target: "UI-Streamer", waitForResponse: false, message: JSON.stringify(message) };
32
67
  window.cefQuery({
@@ -44,18 +79,25 @@ class Overlay extends OverlayInterface {
44
79
  return Promise.reject("Overlay is not supported if NOT running e2e");
45
80
  }
46
81
 
47
- _renderFrame() {
82
+ _renderFrame(conditional = false) {
48
83
  if (!this._element) {
49
84
  return Promise.reject("No element configured");
50
85
  }
51
86
  const rect = this._element.getBoundingClientRect();
52
87
  const frame = this._normalizeFrame(rect);
53
88
 
54
- sdkLogger.log(`Overlay: rendering frame x=${frame.x} y=${frame.y} width=${frame.width} height=${frame.height}`);
55
- return this._sendFrame(frame.x, frame.y, frame.width, frame.height).then(() => {
56
- const refreshEvent = new Event("refresh");
57
- refreshEvent.frame = frame;
58
- this.dispatchEvent(refreshEvent);
89
+ if (frame.width === 0 || frame.height === 0) {
90
+ sdkLogger.warn(`Overlay: skipping frame with zero size (width=${frame.width}, height=${frame.height})`);
91
+ return Promise.resolve(false);
92
+ }
93
+
94
+ sdkLogger.log(`Overlay: rendering frame x=${frame.x} y=${frame.y} width=${frame.width} height=${frame.height} (conditional=${conditional})`);
95
+ return this._sendFrame(frame.x, frame.y, frame.width, frame.height, conditional).then(() => {
96
+ if (!conditional) {
97
+ const refreshEvent = new Event("refresh");
98
+ refreshEvent.frame = frame;
99
+ this.dispatchEvent(refreshEvent);
100
+ }
59
101
  return true;
60
102
  });
61
103
  }
@@ -65,8 +107,8 @@ class Overlay extends OverlayInterface {
65
107
 
66
108
  const x = Math.round(toFiniteNumber(rect.x));
67
109
  const y = Math.round(toFiniteNumber(rect.y));
68
- const width = Math.max(1, Math.round(toFiniteNumber(rect.width)));
69
- const height = Math.max(1, Math.round(toFiniteNumber(rect.height)));
110
+ const width = Math.round(toFiniteNumber(rect.width));
111
+ const height =Math.round(toFiniteNumber(rect.height));
70
112
 
71
113
  return { x, y, width, height };
72
114
  }
@@ -78,6 +120,11 @@ class Overlay extends OverlayInterface {
78
120
  normalizedConfiguration.useTransparency = normalizedConfiguration.useTransparency !== false;
79
121
  }
80
122
 
123
+ if (Object.prototype.hasOwnProperty.call(normalizedConfiguration, "autoRefreshSeconds")) {
124
+ const v = normalizedConfiguration.autoRefreshSeconds;
125
+ normalizedConfiguration.autoRefreshSeconds = (typeof v === "number" && v >= 0) ? v : this._configuration.autoRefreshSeconds;
126
+ }
127
+
81
128
  this._configuration = { ...this._configuration, ...normalizedConfiguration };
82
129
  }
83
130
 
@@ -111,6 +158,7 @@ class Overlay extends OverlayInterface {
111
158
  return Promise.reject(errorMsg);
112
159
  }
113
160
 
161
+ this._clearRetryTimers();
114
162
  this._element = null;
115
163
 
116
164
  return Promise.resolve(true);
@@ -122,11 +170,20 @@ class Overlay extends OverlayInterface {
122
170
  sdkLogger.error(errorMsg);
123
171
  return Promise.reject(errorMsg);
124
172
  }
125
- return this._renderFrame();
173
+ this._clearRetryTimers();
174
+ const result = await this._renderFrame();
175
+ if (result === true) {
176
+ const maxMs = (this._configuration.autoRefreshSeconds ?? 0) * 1000;
177
+ if (maxMs > 0) {
178
+ this._scheduleRetries(maxMs);
179
+ }
180
+ }
181
+ return result;
126
182
  }
127
183
 
128
184
  async removeAllElements() {
129
185
  sdkLogger.log("Overlay: removing all elements");
186
+ this._clearRetryTimers();
130
187
  this._element = null;
131
188
  }
132
189
 
@@ -162,6 +219,7 @@ class Overlay extends OverlayInterface {
162
219
  * @returns {Promise<boolean>} Resolves to true if successful, rejects with error if failed.
163
220
  */
164
221
  hideOverlay() {
222
+ this._clearRetryTimers();
165
223
  return this._sendHideOverlay();
166
224
  }
167
225
 
@@ -1021,7 +1021,7 @@ class RemotePlayer extends RemotePlayerInterface {
1021
1021
  }
1022
1022
  // if load in background is supported, play called while state is in background should include all streams
1023
1023
  const backgroundStreamType = this.textTrackVisibility ? (StreamType.AUDIO | StreamType.VIDEO | StreamType.SUBTITLE) : (StreamType.AUDIO | StreamType.VIDEO);
1024
- const streamType = backgroundLoadSupported ? backgroundStreamType : StreamType.AUDIO;
1024
+ const streamType = (backgroundLoadSupported && !isForegroundState) ? backgroundStreamType : StreamType.AUDIO;
1025
1025
 
1026
1026
  return this._play(streamType, switchMode);
1027
1027
  }
@@ -504,6 +504,18 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
504
504
 
505
505
  }
506
506
 
507
+ _hasActiveTrackForLanguageAndRole(tracks, language, role) {
508
+ const activeTrack = tracks.find(track => track.active);
509
+ if (!activeTrack) {
510
+ return false;
511
+ }
512
+
513
+ if ((!role || activeTrack.roles?.includes(role)) && activeTrack.language === language) {
514
+ return true;
515
+ }
516
+
517
+ return false;
518
+ }
507
519
 
508
520
  _attach(videoElement) {
509
521
  this.videoElement = videoElement;
@@ -525,7 +537,10 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
525
537
  this.handleSenzaError(error.code, error.message || "Unknown play error");
526
538
  throw error;
527
539
  });
528
- return this._originalPlay.call(this.videoElement);
540
+ this._originalPlay.call(this.videoElement).catch(error => {
541
+ this.handleSenzaError(error.code, error.message || "Unknown play error");
542
+ });
543
+ return;
529
544
  }
530
545
 
531
546
  if (this.remotePlayer._isPlaying) {
@@ -620,7 +635,6 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
620
635
  return [...new Set(tracks.map(item => item.lang))];
621
636
  }
622
637
 
623
-
624
638
  selectVariantTrack(track, clearBuffer = false, safeMargin = 0) {
625
639
  const audioLang = track.language;
626
640
  const audioRole = Array.isArray(track.audioRoles) && track.audioRoles.length > 0
@@ -632,8 +646,11 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
632
646
  const apCode = this._getAccessibilityCodeFromPurpose(track.accessibilityPurpose);
633
647
  remotePlayer.selectAudioLanguage(audioLang, audioRole, apCode);
634
648
 
635
- super.selectVariantTrack(track, clearBuffer, safeMargin);
636
-
649
+ if (track && !track.active) {
650
+ super.selectVariantTrack(track, clearBuffer, safeMargin);
651
+ } else {
652
+ sdkLogger.log("selectVariantTrack() skipping local Shaka selection because track is already active");
653
+ }
637
654
  }
638
655
 
639
656
  selectAudioTrack(audioTrack, safeMargin = 0) {
@@ -646,13 +663,24 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
646
663
  const apCode = this._getAccessibilityCodeFromPurpose(audioTrack.accessibilityPurpose);
647
664
  remotePlayer.selectAudioLanguage(audioLang, role, apCode);
648
665
 
649
- super.selectAudioTrack(audioTrack, safeMargin);
666
+ if (audioTrack && !audioTrack.active) {
667
+ super.selectAudioTrack(audioTrack, safeMargin);
668
+ } else {
669
+ sdkLogger.log("selectAudioTrack() skipping local Shaka selection because track is already active");
670
+ }
650
671
  }
651
672
 
652
673
  selectAudioLanguage(language, role) {
653
674
  sdkLogger.log("selectAudioLanguage() Selecting audio language:", language, "with role: ", role);
654
675
  remotePlayer.selectAudioLanguage(language, role);
655
- super.selectAudioLanguage(language, role);
676
+
677
+ const tracks = this.getAudioTracks();
678
+ sdkLogger.log("selectAudioLanguage: audio tracks", JSON.stringify(tracks), "language", language, "role", role);
679
+ if (!this._hasActiveTrackForLanguageAndRole(tracks, language, role)) {
680
+ super.selectAudioLanguage(language, role);
681
+ } else {
682
+ sdkLogger.log("selectAudioLanguage() skipping local Shaka selection because language is already active");
683
+ }
656
684
  }
657
685
 
658
686
  selectTextTrack(textTrack) {
@@ -665,12 +693,22 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
665
693
  const apCode = this._getAccessibilityCodeFromPurpose(textTrack.accessibilityPurpose);
666
694
  remotePlayer.selectTextLanguage(textLang, role, apCode);
667
695
 
668
- super.selectTextTrack(textTrack);
696
+ if (textTrack && !textTrack.active) {
697
+ super.selectTextTrack(textTrack);
698
+ } else {
699
+ sdkLogger.log("selectTextTrack() skipping local Shaka selection because track is already active");
700
+ }
669
701
  }
670
702
  selectTextLanguage(language, role) {
671
703
  sdkLogger.log("selectTextLanguage() Selecting text language:", language, "with role: ", role);
672
704
  remotePlayer.selectTextLanguage(language, role);
673
- super.selectTextLanguage(language, role);
705
+ const tracks = this.getTextTracks();
706
+ sdkLogger.log("selectTextLanguage: text tracks", JSON.stringify(tracks));
707
+ if (!this._hasActiveTrackForLanguageAndRole(tracks, language, role)) {
708
+ super.selectTextLanguage(language, role);
709
+ } else {
710
+ sdkLogger.log("selectTextLanguage() skipping local Shaka selection because language is already active");
711
+ }
674
712
  }
675
713
 
676
714
  setTextTrackVisibility(visible) {
@@ -46,6 +46,14 @@ class Overlay extends EventTarget {
46
46
  * This is useful when the element's content or position has changed.
47
47
  * The overlay must have an element registered before calling refresh.
48
48
  *
49
+ * After the initial capture, conditional retries are automatically scheduled using
50
+ * exponential backoff (starting at 100 ms, doubling each step) until the window
51
+ * configured by `configure({ autoRefreshSeconds })` expires (default: 2 seconds).
52
+ * Each retry is skipped by the UI-Streamer unless the browser has repainted since
53
+ * the last non-conditional capture, eliminating redundant JPEG encodes.
54
+ * Pending retries are cancelled by a subsequent call to refresh(), hideOverlay(),
55
+ * or removeElement().
56
+ *
49
57
  * @returns {Promise<boolean>} Resolves to true if successful, rejects with error if failed.
50
58
  */
51
59
  refresh() {
@@ -77,6 +85,9 @@ class Overlay extends EventTarget {
77
85
  * @param {Object} configuration - The new configuration to apply.
78
86
  * @param {boolean} [configuration.useTransparency=true] - Controls whether the overlay should be rendered with transparency.
79
87
  * When set to `true`, the value is forwarded to UI-Streamer in `displayOverlay` requests.
88
+ * @param {number} [configuration.autoRefreshSeconds=2] - Maximum window in seconds for automatic
89
+ * conditional retries after each refresh() call. Retries are spaced with exponential backoff
90
+ * starting at 100 ms. Set to 0 to disable automatic retries.
80
91
  */
81
92
  configure(configuration) {
82
93
  noop(configuration);
@@ -1 +1 @@
1
- export const version = "4.5.0";
1
+ export const version = "4.5.1";