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/dist/bundle.js +1 -1
- package/dist/implementation.bundle.js +1 -1
- package/package.json +1 -1
- package/src/api.js +4 -3
- package/src/implementation/api.js +5 -3
- package/src/implementation/overlay.js +143 -36
- package/src/implementation/remotePlayer.js +54 -12
- package/src/implementation/utils.js +13 -0
- package/src/interface/overlay.js +12 -10
- package/src/interface/version.js +1 -1
package/package.json
CHANGED
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
24
|
-
this._retryTimerId = null;
|
|
55
|
+
this._clearPlanTimer();
|
|
25
56
|
}
|
|
26
57
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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(
|
|
108
|
+
await this._renderFrame(batchId, step, index);
|
|
41
109
|
} catch (err) {
|
|
42
|
-
sdkLogger.
|
|
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
|
-
|
|
45
|
-
scheduleNext();
|
|
46
|
-
}, delay);
|
|
115
|
+
}, step.delay);
|
|
47
116
|
};
|
|
48
117
|
|
|
49
|
-
|
|
118
|
+
scheduleStep(startIndex);
|
|
50
119
|
}
|
|
51
120
|
|
|
52
|
-
|
|
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 =
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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, "
|
|
124
|
-
const
|
|
125
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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);
|
package/src/interface/overlay.js
CHANGED
|
@@ -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
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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 {
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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);
|
package/src/interface/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "4.5.
|
|
1
|
+
export const version = "4.5.3";
|