senza-sdk 4.5.0 → 4.5.2

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.2",
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;
@@ -1,24 +1,131 @@
1
1
  import { Overlay as OverlayInterface } from "../interface/overlay.js";
2
2
  import { getFCID, sdkLogger, SenzaError } from "./utils";
3
3
 
4
+ /**
5
+ * Named overlay capture plans. Each step: `{ quality, conditional, delay }`.
6
+ * Steps run when refresh() is called; `delay` is ms after the previous step in the batch.
7
+ * `conditional`: when true, capture only if overlay content changed since the last
8
+ * required (non-conditional) capture in this batch; when false, always capture on that step.
9
+ * Set per step by the plan author; the SDK does not derive it.
10
+ */
11
+ const OVERLAY_CAPTURE_PRESETS = {
12
+ default: [
13
+ { quality: "low", conditional: false, delay: 0 },
14
+ { quality: "low", conditional: true, delay: 150 },
15
+ { quality: "low", conditional: true, delay: 300 },
16
+ { quality: "high", conditional: false, delay: 600 }
17
+ ],
18
+ once: [
19
+ { quality: "high", conditional: true, delay: 0 }
20
+ ]
21
+ };
22
+
23
+ const DEFAULT_CAPTURE_PRESET = "default";
24
+ const VALID_QUALITIES = new Set(["low", "mid", "high"]);
25
+
4
26
  class Overlay extends OverlayInterface {
5
27
  constructor() {
6
28
  super();
7
29
 
8
30
  this._element = null;
9
31
  this._configuration = {
10
- useTransparency: true
32
+ useTransparency: true,
33
+ overlayCapturePreset: DEFAULT_CAPTURE_PRESET,
34
+ overlayCapturePlan: null
11
35
  };
36
+ this._activeCapturePlan = OVERLAY_CAPTURE_PRESETS[DEFAULT_CAPTURE_PRESET];
37
+ this._retryGeneration = 0;
38
+ this._planTimerId = null;
39
+ this._currentBatchId = null;
12
40
  }
13
41
 
14
42
  _isTransparencyEnabled() {
15
43
  return this._configuration.useTransparency !== false;
16
44
  }
17
45
 
18
- _sendFrame(x, y, width, height) {
46
+ _clearPlanTimer() {
47
+ if (this._planTimerId != null) {
48
+ clearTimeout(this._planTimerId);
49
+ this._planTimerId = null;
50
+ }
51
+ }
52
+
53
+ _clearRetryTimers() {
54
+ this._retryGeneration++;
55
+ this._clearPlanTimer();
56
+ }
57
+
58
+ _validateCapturePlan(plan) {
59
+ if (!Array.isArray(plan) || plan.length === 0) {
60
+ return false;
61
+ }
62
+ for (let i = 0; i < plan.length; i++) {
63
+ const step = plan[i];
64
+ if (!step || typeof step !== "object") {
65
+ return false;
66
+ }
67
+ if (!VALID_QUALITIES.has(step.quality)) {
68
+ return false;
69
+ }
70
+ if (typeof step.conditional !== "boolean") {
71
+ return false;
72
+ }
73
+ if (typeof step.delay !== "number" || !Number.isFinite(step.delay) || step.delay < 0) {
74
+ return false;
75
+ }
76
+ }
77
+ return true;
78
+ }
79
+
80
+ _resolveCapturePlan() {
81
+ if (this._configuration.overlayCapturePlan != null) {
82
+ return structuredClone(this._configuration.overlayCapturePlan);
83
+ }
84
+ const presetName = this._configuration.overlayCapturePreset ?? DEFAULT_CAPTURE_PRESET;
85
+ const preset = OVERLAY_CAPTURE_PRESETS[presetName];
86
+ if (!preset) {
87
+ throw new Error(`Overlay: unknown overlayCapturePreset "${presetName}"`);
88
+ }
89
+ return structuredClone(preset);
90
+ }
91
+
92
+ _scheduleCapturePlanFromStep(batchId, plan, generation, startIndex) {
93
+ const scheduleStep = (index) => {
94
+ if (this._retryGeneration !== generation) {
95
+ return;
96
+ }
97
+ const step = plan[index];
98
+ if (!step) {
99
+ sdkLogger.error(`Overlay: missing capture plan step at index ${index} (plan length ${plan.length})`);
100
+ return;
101
+ }
102
+ this._clearPlanTimer();
103
+ this._planTimerId = setTimeout(async () => {
104
+ if (this._retryGeneration !== generation) {
105
+ return;
106
+ }
107
+ try {
108
+ await this._renderFrame(batchId, step, index);
109
+ } catch (err) {
110
+ sdkLogger.error("Overlay: capture plan step error:", err);
111
+ }
112
+ if (index + 1 < plan.length && this._retryGeneration === generation) {
113
+ scheduleStep(index + 1);
114
+ }
115
+ }, step.delay);
116
+ };
117
+
118
+ scheduleStep(startIndex);
119
+ }
120
+
121
+ /**
122
+ * @param {boolean} conditional - When true, capture only if overlay content changed since
123
+ * the last required capture in this batch; when false, always capture on this step.
124
+ */
125
+ _sendFrame(x, y, width, height, batchId, quality, conditional, stepIndex = 0) {
19
126
  if (window.cefQuery) {
20
127
  return new Promise((resolve, reject) => {
21
- const FCID = getFCID();
128
+ const FCID = `${stepIndex}-${getFCID()}`;
22
129
  const message = {
23
130
  type: "displayOverlay",
24
131
  x,
@@ -26,8 +133,12 @@ class Overlay extends OverlayInterface {
26
133
  width,
27
134
  height,
28
135
  useTransparency: this._isTransparencyEnabled(),
29
- fcid: FCID
136
+ fcid: FCID,
137
+ batchId,
138
+ quality,
139
+ conditional
30
140
  };
141
+ sdkLogger.log(`Overlay: rendering frame x=${x} y=${y} width=${width} height=${height} fcid=${FCID} (batchId=${batchId}, quality=${quality}, conditional=${conditional})`);
31
142
  const request = { target: "UI-Streamer", waitForResponse: false, message: JSON.stringify(message) };
32
143
  window.cefQuery({
33
144
  request: JSON.stringify(request),
@@ -44,18 +155,26 @@ class Overlay extends OverlayInterface {
44
155
  return Promise.reject("Overlay is not supported if NOT running e2e");
45
156
  }
46
157
 
47
- _renderFrame() {
158
+ _renderFrame(batchId, captureStep = { quality: "high", conditional: false }, stepIndex = 0) {
48
159
  if (!this._element) {
49
160
  return Promise.reject("No element configured");
50
161
  }
51
162
  const rect = this._element.getBoundingClientRect();
52
163
  const frame = this._normalizeFrame(rect);
53
164
 
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);
165
+ if (frame.width === 0 || frame.height === 0) {
166
+ sdkLogger.warn(`Overlay: skipping frame with zero size (width=${frame.width}, height=${frame.height})`);
167
+ return Promise.resolve(false);
168
+ }
169
+
170
+ const id = batchId ?? this._currentBatchId ?? getFCID();
171
+
172
+ return this._sendFrame(frame.x, frame.y, frame.width, frame.height, id, captureStep.quality, captureStep.conditional, stepIndex).then(() => {
173
+ if (!captureStep.conditional) {
174
+ const refreshEvent = new Event("refresh");
175
+ refreshEvent.frame = frame;
176
+ this.dispatchEvent(refreshEvent);
177
+ }
59
178
  return true;
60
179
  });
61
180
  }
@@ -65,8 +184,8 @@ class Overlay extends OverlayInterface {
65
184
 
66
185
  const x = Math.round(toFiniteNumber(rect.x));
67
186
  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)));
187
+ const width = Math.round(toFiniteNumber(rect.width));
188
+ const height = Math.round(toFiniteNumber(rect.height));
70
189
 
71
190
  return { x, y, width, height };
72
191
  }
@@ -74,11 +193,41 @@ class Overlay extends OverlayInterface {
74
193
  _applyConfiguration(configuration) {
75
194
  const normalizedConfiguration = { ...configuration };
76
195
 
196
+ if (
197
+ Object.prototype.hasOwnProperty.call(normalizedConfiguration, "overlayCapturePreset") ||
198
+ Object.prototype.hasOwnProperty.call(normalizedConfiguration, "overlayCapturePlan")
199
+ ) {
200
+ this._clearRetryTimers();
201
+ }
202
+
77
203
  if (Object.prototype.hasOwnProperty.call(normalizedConfiguration, "useTransparency")) {
78
204
  normalizedConfiguration.useTransparency = normalizedConfiguration.useTransparency !== false;
79
205
  }
80
206
 
207
+ if (Object.prototype.hasOwnProperty.call(normalizedConfiguration, "overlayCapturePreset")) {
208
+ const preset = normalizedConfiguration.overlayCapturePreset;
209
+ if (typeof preset === "string" && OVERLAY_CAPTURE_PRESETS[preset]) {
210
+ normalizedConfiguration.overlayCapturePreset = preset;
211
+ } else if (preset != null) {
212
+ sdkLogger.warn(`Overlay: invalid overlayCapturePreset "${preset}", keeping previous preset`);
213
+ delete normalizedConfiguration.overlayCapturePreset;
214
+ }
215
+ }
216
+
217
+ if (Object.prototype.hasOwnProperty.call(normalizedConfiguration, "overlayCapturePlan")) {
218
+ const plan = normalizedConfiguration.overlayCapturePlan;
219
+ if (plan == null) {
220
+ normalizedConfiguration.overlayCapturePlan = null;
221
+ } else if (this._validateCapturePlan(plan)) {
222
+ normalizedConfiguration.overlayCapturePlan = structuredClone(plan);
223
+ } else {
224
+ sdkLogger.warn("Overlay: invalid overlayCapturePlan, keeping previous plan");
225
+ delete normalizedConfiguration.overlayCapturePlan;
226
+ }
227
+ }
228
+
81
229
  this._configuration = { ...this._configuration, ...normalizedConfiguration };
230
+ this._activeCapturePlan = this._resolveCapturePlan();
82
231
  }
83
232
 
84
233
  async addElement(element) {
@@ -111,6 +260,7 @@ class Overlay extends OverlayInterface {
111
260
  return Promise.reject(errorMsg);
112
261
  }
113
262
 
263
+ this._clearRetryTimers();
114
264
  this._element = null;
115
265
 
116
266
  return Promise.resolve(true);
@@ -122,11 +272,25 @@ class Overlay extends OverlayInterface {
122
272
  sdkLogger.error(errorMsg);
123
273
  return Promise.reject(errorMsg);
124
274
  }
125
- return this._renderFrame();
275
+ this._clearRetryTimers();
276
+ this._currentBatchId = getFCID();
277
+ const batchId = this._currentBatchId;
278
+ const generation = this._retryGeneration;
279
+ const plan = this._activeCapturePlan;
280
+ if (plan[0].delay === 0) {
281
+ const result = await this._renderFrame(batchId, plan[0], 0);
282
+ if (result === true && plan.length > 1) {
283
+ this._scheduleCapturePlanFromStep(batchId, plan, generation, 1);
284
+ }
285
+ return result;
286
+ }
287
+ this._scheduleCapturePlanFromStep(batchId, plan, generation, 0);
288
+ return true;
126
289
  }
127
290
 
128
291
  async removeAllElements() {
129
292
  sdkLogger.log("Overlay: removing all elements");
293
+ this._clearRetryTimers();
130
294
  this._element = null;
131
295
  }
132
296
 
@@ -162,6 +326,7 @@ class Overlay extends OverlayInterface {
162
326
  * @returns {Promise<boolean>} Resolves to true if successful, rejects with error if failed.
163
327
  */
164
328
  hideOverlay() {
329
+ this._clearRetryTimers();
165
330
  return this._sendHideOverlay();
166
331
  }
167
332
 
@@ -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,12 @@ 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
+ * Runs the configured overlay capture plan after each call. The `default` preset sends
50
+ * low-quality captures with relative delays, ending with a high-quality capture.
51
+ * The `once` preset runs one high-quality conditional step immediately.
52
+ * All steps in one refresh share the same `batchId` (from `getFCID()`). Pending plan
53
+ * steps are cancelled by a subsequent call to refresh(), hideOverlay(), or removeElement().
54
+ *
49
55
  * @returns {Promise<boolean>} Resolves to true if successful, rejects with error if failed.
50
56
  */
51
57
  refresh() {
@@ -77,6 +83,13 @@ class Overlay extends EventTarget {
77
83
  * @param {Object} configuration - The new configuration to apply.
78
84
  * @param {boolean} [configuration.useTransparency=true] - Controls whether the overlay should be rendered with transparency.
79
85
  * When set to `true`, the value is forwarded to UI-Streamer in `displayOverlay` requests.
86
+ * @param {string} [configuration.overlayCapturePreset="default"] - Named capture plan preset:
87
+ * `default` (progressive low→high captures) or `once` (one immediate high-quality conditional step).
88
+ * @param {Array<{quality: "low"|"mid"|"high", conditional: boolean, delay: number}>} [configuration.overlayCapturePlan]
89
+ * Explicit capture plan; overrides preset. Steps run on refresh(); each step's `delay` is ms after
90
+ * the previous step in the batch. Set `conditional` per step (the SDK does not derive it):
91
+ * when `true`, capture only if overlay content changed since the last required (non-conditional)
92
+ * capture in this batch; when `false`, always capture on that step.
80
93
  */
81
94
  configure(configuration) {
82
95
  noop(configuration);
@@ -1 +1 @@
1
- export const version = "4.5.0";
1
+ export const version = "4.5.2";