senza-sdk 4.5.1 → 4.5.3

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.1",
3
+ "version": "4.5.3",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
package/src/api.js CHANGED
@@ -153,12 +153,13 @@ export const envInfo = {
153
153
  * @param {ArrayBuffer|string} licenseResponse a license response that was received from the license server to be passed to platform.
154
154
  * @param {string} fcid a fcid received with the license request
155
155
  * @param {string} sessionId a sessionId received with the license request
156
- * In case of success licenceResponse is of type @type {ArrayBuffer}
156
+ * @param {string} playbackId the current playbackId, propagated to platform for session correlation
157
+ * In case of success licenseResponse is of type @type {ArrayBuffer}
157
158
  * In case of error licenseResponse is of type @type {string}
158
159
  */
159
- export function writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId) {
160
+ export function writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId, playbackId) {
160
161
  if (window?.senzaSDKImplementation) {
161
- return window.senzaSDKImplementation.writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId);
162
+ return window.senzaSDKImplementation.writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId, playbackId);
162
163
  }
163
164
  sdkLogger.error("writeLicenseResponse: window.cefQuery is undefined");
164
165
  }
@@ -303,10 +303,11 @@ import "./devHelper.js";
303
303
  * @param {ArrayBuffer|string} licenseResponse a license response that was received from the license server to be passed to platform.
304
304
  * @param {string} fcid a fcid received with the license request
305
305
  * @param {string} sessionId a sessionId received with the license request
306
- * In case of success licenceResponse is of type @type {ArrayBuffer}
306
+ * @param {string} playbackId the current playbackId, propagated to platform for session correlation
307
+ * In case of success licenseResponse is of type @type {ArrayBuffer}
307
308
  * In case of error licenseResponse is of type @type {string}
308
309
  */
309
- export function writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId) {
310
+ export function writeLicenseResponse(statusCode, licenseResponse, fcid, sessionId, playbackId) {
310
311
 
311
312
  if (statusCode >= 200 && statusCode < 300) {
312
313
  licenseResponse = window.btoa(String.fromCharCode.apply(null, new Uint8Array(licenseResponse))); // to base64
@@ -316,7 +317,8 @@ export function writeLicenseResponse(statusCode, licenseResponse, fcid, sessionI
316
317
  const message = {
317
318
  type: "updateLicense",
318
319
  sessionId,
319
- fcid
320
+ fcid,
321
+ playbackId
320
322
  };
321
323
  message[statusCode >= 200 && statusCode < 300 ? "response" : "error"] = licenseResponse;
322
324
  const request = { target: "TC", waitForResponse: false, message: JSON.stringify(message) };
@@ -1,6 +1,28 @@
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();
@@ -8,51 +30,102 @@ class Overlay extends OverlayInterface {
8
30
  this._element = null;
9
31
  this._configuration = {
10
32
  useTransparency: true,
11
- autoRefreshSeconds: 2
33
+ overlayCapturePreset: DEFAULT_CAPTURE_PRESET,
34
+ overlayCapturePlan: null
12
35
  };
36
+ this._activeCapturePlan = OVERLAY_CAPTURE_PRESETS[DEFAULT_CAPTURE_PRESET];
13
37
  this._retryGeneration = 0;
14
- this._retryTimerId = null;
38
+ this._planTimerId = null;
39
+ this._currentBatchId = null;
15
40
  }
16
41
 
17
42
  _isTransparencyEnabled() {
18
43
  return this._configuration.useTransparency !== false;
19
44
  }
20
45
 
46
+ _clearPlanTimer() {
47
+ if (this._planTimerId != null) {
48
+ clearTimeout(this._planTimerId);
49
+ this._planTimerId = null;
50
+ }
51
+ }
52
+
21
53
  _clearRetryTimers() {
22
54
  this._retryGeneration++;
23
- clearTimeout(this._retryTimerId);
24
- this._retryTimerId = null;
55
+ this._clearPlanTimer();
25
56
  }
26
57
 
27
- _scheduleRetries(maxMs) {
28
- const startTime = Date.now();
29
- let delay = 100;
30
- const generation = ++this._retryGeneration;
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
+ }
31
79
 
32
- const scheduleNext = () => {
33
- if (this._retryGeneration !== generation) return;
34
- const elapsed = Date.now() - startTime;
35
- if (elapsed + delay > maxMs) return;
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
+ }
36
91
 
37
- this._retryTimerId = setTimeout(async () => {
38
- if (this._retryGeneration !== generation) return;
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
+ }
39
107
  try {
40
- await this._renderFrame(true);
108
+ await this._renderFrame(batchId, step, index);
41
109
  } catch (err) {
42
- sdkLogger.log(`Overlay: conditional refresh error: ${err}`);
110
+ sdkLogger.error("Overlay: capture plan step error:", err);
111
+ }
112
+ if (index + 1 < plan.length && this._retryGeneration === generation) {
113
+ scheduleStep(index + 1);
43
114
  }
44
- delay *= 2;
45
- scheduleNext();
46
- }, delay);
115
+ }, step.delay);
47
116
  };
48
117
 
49
- scheduleNext();
118
+ scheduleStep(startIndex);
50
119
  }
51
120
 
52
- _sendFrame(x, y, width, height, conditional = false) {
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) {
53
126
  if (window.cefQuery) {
54
127
  return new Promise((resolve, reject) => {
55
- const FCID = getFCID();
128
+ const FCID = `${stepIndex}-${getFCID()}`;
56
129
  const message = {
57
130
  type: "displayOverlay",
58
131
  x,
@@ -61,8 +134,11 @@ class Overlay extends OverlayInterface {
61
134
  height,
62
135
  useTransparency: this._isTransparencyEnabled(),
63
136
  fcid: FCID,
137
+ batchId,
138
+ quality,
64
139
  conditional
65
140
  };
141
+ sdkLogger.log(`Overlay: rendering frame x=${x} y=${y} width=${width} height=${height} fcid=${FCID} (batchId=${batchId}, quality=${quality}, conditional=${conditional})`);
66
142
  const request = { target: "UI-Streamer", waitForResponse: false, message: JSON.stringify(message) };
67
143
  window.cefQuery({
68
144
  request: JSON.stringify(request),
@@ -79,7 +155,7 @@ class Overlay extends OverlayInterface {
79
155
  return Promise.reject("Overlay is not supported if NOT running e2e");
80
156
  }
81
157
 
82
- _renderFrame(conditional = false) {
158
+ _renderFrame(batchId, captureStep = { quality: "high", conditional: false }, stepIndex = 0) {
83
159
  if (!this._element) {
84
160
  return Promise.reject("No element configured");
85
161
  }
@@ -91,9 +167,10 @@ class Overlay extends OverlayInterface {
91
167
  return Promise.resolve(false);
92
168
  }
93
169
 
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) {
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) {
97
174
  const refreshEvent = new Event("refresh");
98
175
  refreshEvent.frame = frame;
99
176
  this.dispatchEvent(refreshEvent);
@@ -108,7 +185,7 @@ class Overlay extends OverlayInterface {
108
185
  const x = Math.round(toFiniteNumber(rect.x));
109
186
  const y = Math.round(toFiniteNumber(rect.y));
110
187
  const width = Math.round(toFiniteNumber(rect.width));
111
- const height =Math.round(toFiniteNumber(rect.height));
188
+ const height = Math.round(toFiniteNumber(rect.height));
112
189
 
113
190
  return { x, y, width, height };
114
191
  }
@@ -116,16 +193,41 @@ class Overlay extends OverlayInterface {
116
193
  _applyConfiguration(configuration) {
117
194
  const normalizedConfiguration = { ...configuration };
118
195
 
196
+ if (
197
+ Object.prototype.hasOwnProperty.call(normalizedConfiguration, "overlayCapturePreset") ||
198
+ Object.prototype.hasOwnProperty.call(normalizedConfiguration, "overlayCapturePlan")
199
+ ) {
200
+ this._clearRetryTimers();
201
+ }
202
+
119
203
  if (Object.prototype.hasOwnProperty.call(normalizedConfiguration, "useTransparency")) {
120
204
  normalizedConfiguration.useTransparency = normalizedConfiguration.useTransparency !== false;
121
205
  }
122
206
 
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;
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
+ }
126
227
  }
127
228
 
128
229
  this._configuration = { ...this._configuration, ...normalizedConfiguration };
230
+ this._activeCapturePlan = this._resolveCapturePlan();
129
231
  }
130
232
 
131
233
  async addElement(element) {
@@ -171,14 +273,19 @@ class Overlay extends OverlayInterface {
171
273
  return Promise.reject(errorMsg);
172
274
  }
173
275
  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);
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);
179
284
  }
285
+ return result;
180
286
  }
181
- return result;
287
+ this._scheduleCapturePlanFromStep(batchId, plan, generation, 0);
288
+ return true;
182
289
  }
183
290
 
184
291
  async removeAllElements() {
@@ -2,6 +2,7 @@
2
2
  import { RemotePlayer as RemotePlayerInterface, RemotePlayerError as RemotePlayerErrorInterface, Config } from "../interface/remotePlayer";
3
3
  import {
4
4
  getFCID,
5
+ generatePlaybackId,
5
6
  isAudioSyncConfigured,
6
7
  clearTimer,
7
8
  sdkLogger,
@@ -145,6 +146,13 @@ class RemotePlayer extends RemotePlayerInterface {
145
146
  * @private
146
147
  */
147
148
  this._isPlaying = false;
149
+
150
+ /**
151
+ * @type {string}
152
+ * @description Unique identifier generated on every load attempt. Propagated on all API requests and events.
153
+ * @private
154
+ */
155
+ this._playbackId = "";
148
156
  }
149
157
 
150
158
  /** @private Initialize the remote player
@@ -219,17 +227,33 @@ class RemotePlayer extends RemotePlayerInterface {
219
227
 
220
228
  }
221
229
 
230
+ /**
231
+ * @private Returns true and logs an error when an incoming event's playbackId does not match the current one.
232
+ * @param {Object} detail - The event detail object that may contain a playbackId field.
233
+ * @param {string} eventType - Name of the incoming event, used in the log message.
234
+ */
235
+ _hasPlaybackIdMismatch(detail, eventType) {
236
+ const incomingId = detail?.playbackId;
237
+ if (incomingId && this._playbackId && incomingId !== this._playbackId) {
238
+ sdkLogger.error(`${eventType}: playbackId mismatch. current=${this._playbackId} incoming=${incomingId}. Ignoring.`);
239
+ return true;
240
+ }
241
+ return false;
242
+ }
243
+
222
244
  /**
223
245
  * @private Add event listeners for system events
224
246
  */
225
247
  _addSenzaEventListeners() {
226
248
 
227
249
  typeof document !== "undefined" && document.addEventListener("hs/remotePlayerEvent", (e) => {
250
+ if (this._hasPlaybackIdMismatch(e?.detail, "hs/remotePlayerEvent")) return;
228
251
  sdkLogger.info("Got hs/remotePlayerEvent event with detail", JSON.stringify(e?.detail));
229
252
  this.dispatchEvent(new Event(e?.detail?.eventName));
230
253
  });
231
254
 
232
- typeof document !== "undefined" && document.addEventListener("hs/playbackInfoEvent", () => {
255
+ typeof document !== "undefined" && document.addEventListener("hs/playbackInfoEvent", (e) => {
256
+ if (this._hasPlaybackIdMismatch(e?.detail, "hs/playbackInfoEvent")) return;
233
257
  sdkLogger.info("Got hs/playbackInfoEvent");
234
258
  // When attached, the sdk controls the synchronization between the local and remote player.
235
259
  if (this._videoElement) {
@@ -243,13 +267,15 @@ class RemotePlayer extends RemotePlayerInterface {
243
267
  });
244
268
 
245
269
  typeof document !== "undefined" && document.addEventListener("hs/playback", (e) => {
270
+ if (this._hasPlaybackIdMismatch(e?.detail, "hs/playback")) return;
246
271
  sdkLogger.info("Got hs/playback event with detail", JSON.stringify(e?.detail));
247
272
  this._availabilityStartTime = e?.detail?.availabilityStartTime;
248
273
  this._updateTracks(e?.detail);
249
274
  this.dispatchEvent(new Event("tracksupdate"));
250
275
  });
251
276
 
252
- typeof document !== "undefined" && document.addEventListener("hs/EOS", () => {
277
+ typeof document !== "undefined" && document.addEventListener("hs/EOS", (e) => {
278
+ if (this._hasPlaybackIdMismatch(e?.detail, "hs/EOS")) return;
253
279
  sdkLogger.info("Got hs/EOS event");
254
280
  this._changePlayMode(false);
255
281
  this.dispatchEvent(new Event("ended"));
@@ -268,6 +294,7 @@ class RemotePlayer extends RemotePlayerInterface {
268
294
  }
269
295
  });
270
296
  typeof document !== "undefined" && document.addEventListener("hs/senzaPlayerSetRate", (event) => {
297
+ if (this._hasPlaybackIdMismatch(event?.detail, "hs/senzaPlayerSetRate")) return;
271
298
  if (!this._videoElement) return;
272
299
  if (this._isSeekingByApplication) {
273
300
  sdkLogger.info("Skip senzaPlayerSetRate while seek performed");
@@ -276,6 +303,7 @@ class RemotePlayer extends RemotePlayerInterface {
276
303
  this._videoElement.playbackRate = event.detail.rate;
277
304
  });
278
305
  typeof document !== "undefined" && document.addEventListener("hs/senzaPlayerSetTime", (event) => {
306
+ if (this._hasPlaybackIdMismatch(event?.detail, "hs/senzaPlayerSetTime")) return;
279
307
  // For simplicity, make sure we only accept syncing the current time if audio sync is enabled
280
308
  if (!this._isAudioSyncEnabled()) return;
281
309
  if (this._isSeekingByApplication) {
@@ -291,6 +319,7 @@ class RemotePlayer extends RemotePlayerInterface {
291
319
 
292
320
 
293
321
  typeof document !== "undefined" && document.addEventListener("hs/ERR", (event) => {
322
+ if (this._hasPlaybackIdMismatch(event?.detail, "hs/ERR")) return;
294
323
  sdkLogger.info("Got hs/ERR event");
295
324
  delete event?.detail?.type; // type is always videoPlaybackEvent, so no need to pass it
296
325
  delete event?.detail?.eventCode; // eventCode is always ERR, so no need to pass it
@@ -298,6 +327,7 @@ class RemotePlayer extends RemotePlayerInterface {
298
327
  });
299
328
 
300
329
  typeof document !== "undefined" && document.addEventListener("hs/getLicense", (event) => {
330
+ if (this._hasPlaybackIdMismatch(event?.detail, "hs/getLicense")) return;
301
331
  sdkLogger.info("Got hs/getLicense event");
302
332
  const getLicenseEventData = event?.detail;
303
333
 
@@ -308,9 +338,10 @@ class RemotePlayer extends RemotePlayerInterface {
308
338
  this.licenseRequest = licenseRequest;
309
339
  const fcid = getLicenseEventData.fcid;
310
340
  const sessionId = getLicenseEventData.sessionId;
341
+ const playbackId = this._playbackId;
311
342
  const licenseRequestEvent = new CustomEvent("license-request", { "detail": { licenseRequest } });
312
343
  licenseRequestEvent.writeLicenseResponse = (statusCode, responseBody) => {
313
- writeLicenseResponse(statusCode, responseBody, fcid, sessionId);
344
+ writeLicenseResponse(statusCode, responseBody, fcid, sessionId, playbackId);
314
345
  };
315
346
  this.dispatchEvent(licenseRequestEvent);
316
347
  });
@@ -485,7 +516,8 @@ class RemotePlayer extends RemotePlayerInterface {
485
516
  class: "remotePlayer",
486
517
  action: "seek",
487
518
  playbackPosition,
488
- fcid: FCID
519
+ fcid: FCID,
520
+ playbackId: this._playbackId
489
521
  };
490
522
  const request = { target: "TC", waitForResponse: waitForResponse, message: JSON.stringify(message) };
491
523
  return new Promise((resolve, reject) => {
@@ -544,7 +576,8 @@ class RemotePlayer extends RemotePlayerInterface {
544
576
  fcid: FCID,
545
577
  audioLanguage,
546
578
  subtitlesLanguage,
547
- playbackPosition: this.currentTime
579
+ playbackPosition: this.currentTime,
580
+ playbackId: this._playbackId
548
581
  };
549
582
  let waitForResponse = false;
550
583
  if (this._remotePlayerApiVersion >= 2) {
@@ -611,7 +644,8 @@ class RemotePlayer extends RemotePlayerInterface {
611
644
  type: "remotePlayer.pause",
612
645
  class: "remotePlayer",
613
646
  action: "pause",
614
- fcid: FCID
647
+ fcid: FCID,
648
+ playbackId: this._playbackId
615
649
  };
616
650
  if (this._remotePlayerApiVersion >= 2) {
617
651
  message.streamType = this._isAudioSyncEnabled() && isForegroundState ? StreamType.AUDIO : StreamType.AUDIO | StreamType.VIDEO | StreamType.SUBTITLE;
@@ -662,7 +696,8 @@ class RemotePlayer extends RemotePlayerInterface {
662
696
  class: "remotePlayer",
663
697
  action: "stop",
664
698
  streamType: streamType,
665
- fcid: FCID
699
+ fcid: FCID,
700
+ playbackId: this._playbackId
666
701
  };
667
702
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
668
703
  return new Promise((resolve, reject) => {
@@ -784,6 +819,7 @@ class RemotePlayer extends RemotePlayerInterface {
784
819
  this._abortSeeking = true;
785
820
  if (reset) {
786
821
  this._reset();
822
+ this._playbackId = generatePlaybackId();
787
823
  }
788
824
  const previousLoadMode = this._loadMode;
789
825
  this._changeLoadMode(this.LoadMode.LOADING);
@@ -804,7 +840,8 @@ class RemotePlayer extends RemotePlayerInterface {
804
840
  timeout: this._remotePlayerConfirmationTimeout,
805
841
  autoPlay: false,
806
842
  playbackPosition,
807
- fcid: FCID
843
+ fcid: FCID,
844
+ playbackId: this._playbackId
808
845
  };
809
846
  if (this._uiAvSyncIntervalMs !== undefined) {
810
847
  message.connectorSettings = message.connectorSettings || {};
@@ -905,7 +942,8 @@ class RemotePlayer extends RemotePlayerInterface {
905
942
  type: "remotePlayer.unload",
906
943
  class: "remotePlayer",
907
944
  action: "unload",
908
- fcid: FCID
945
+ fcid: FCID,
946
+ playbackId: this._playbackId
909
947
  };
910
948
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
911
949
  let timerId = 0;
@@ -918,6 +956,7 @@ class RemotePlayer extends RemotePlayerInterface {
918
956
  logger.withFields({ duration }).log(`unload completed successfully after ${duration} ms`);
919
957
  timerId = clearTimer(timerId);
920
958
  this._reset();
959
+ this._playbackId = "";
921
960
  this._changeLoadMode(this.LoadMode.NOT_LOADED);
922
961
  this._loadedUrl = undefined;
923
962
  resolve();
@@ -1240,7 +1279,8 @@ class RemotePlayer extends RemotePlayerInterface {
1240
1279
  class: "remotePlayer",
1241
1280
  action: "setAudioLanguage",
1242
1281
  fcid: FCID,
1243
- language: audioTrackId
1282
+ language: audioTrackId,
1283
+ playbackId: this._playbackId
1244
1284
  };
1245
1285
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
1246
1286
  return new Promise((resolve, reject) => {
@@ -1387,7 +1427,8 @@ class RemotePlayer extends RemotePlayerInterface {
1387
1427
  class: "remotePlayer",
1388
1428
  action: "setSubtitleLanguage",
1389
1429
  fcid: FCID,
1390
- language: textTrackId
1430
+ language: textTrackId,
1431
+ playbackId: this._playbackId
1391
1432
  };
1392
1433
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
1393
1434
  return new Promise((resolve, reject) => {
@@ -1889,7 +1930,8 @@ class RemotePlayer extends RemotePlayerInterface {
1889
1930
  class: "remotePlayer",
1890
1931
  action: "screenBlackout",
1891
1932
  fcid: FCID,
1892
- blackoutTime
1933
+ blackoutTime,
1934
+ playbackId: this._playbackId
1893
1935
  };
1894
1936
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
1895
1937
  return new Promise((resolve, reject) => {
@@ -14,6 +14,19 @@ export function getFCID() {
14
14
  return Math.round(Math.random() * 100000) + "-" + getPlatformInfo().sessionInfo?.connectionId;
15
15
  }
16
16
 
17
+ export function generatePlaybackId() {
18
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
19
+ const size = 5;
20
+ let id = "";
21
+ const bytes = (typeof crypto !== "undefined" && crypto.getRandomValues)
22
+ ? crypto.getRandomValues(new Uint8Array(size))
23
+ : Array.from({ length: size }, () => Math.floor(Math.random() * 256));
24
+ for (let i = 0; i < size; i++) {
25
+ id += alphabet[bytes[i] & 63];
26
+ }
27
+ return id;
28
+ }
29
+
17
30
  export class SenzaError extends Error {
18
31
  constructor(code, message) {
19
32
  super(message);
@@ -46,13 +46,11 @@ 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().
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().
56
54
  *
57
55
  * @returns {Promise<boolean>} Resolves to true if successful, rejects with error if failed.
58
56
  */
@@ -85,9 +83,13 @@ class Overlay extends EventTarget {
85
83
  * @param {Object} configuration - The new configuration to apply.
86
84
  * @param {boolean} [configuration.useTransparency=true] - Controls whether the overlay should be rendered with transparency.
87
85
  * 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.
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.
91
93
  */
92
94
  configure(configuration) {
93
95
  noop(configuration);
@@ -1 +1 @@
1
- export const version = "4.5.1";
1
+ export const version = "4.5.3";