senza-sdk 4.2.59 → 4.2.62

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.2.59",
3
+ "version": "4.2.62",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
@@ -14,7 +14,8 @@
14
14
  "prepublish": "npm run build",
15
15
  "eslint": "eslint --max-warnings 0 src test",
16
16
  "build": "npx webpack --config webpack.config.js",
17
- "test": "jest --coverage"
17
+ "test": "jest --coverage --verbose",
18
+ "testall" : "jest --coverage --verbose && npm run eslint --fix"
18
19
  },
19
20
  "devDependencies": {
20
21
  "@babel/cli": "^7.13.16",
@@ -1,6 +1,6 @@
1
1
  import { getFCID, sdkLogger, getRestResponse } from "./utils";
2
- import {isRunningE2E} from "./api";
3
- import {sessionInfo} from "./SessionInfo";
2
+ import { isRunningE2E } from "./api";
3
+ import { sessionInfo } from "./SessionInfo";
4
4
 
5
5
  const wifiInfo = {};
6
6
  let wifi_ap_data;
@@ -290,7 +290,7 @@ class DeviceManager extends EventTarget {
290
290
  // This api is part of epic HSDEV-4185
291
291
  async getWifiInfo() {
292
292
  await Promise.all([getWifiApData(), getWifiStatus()]);
293
- return {...wifi_ap_data, ...wifi_status};
293
+ return { ...wifi_ap_data, ...wifi_status };
294
294
  }
295
295
  }
296
296
 
package/src/lifecycle.js CHANGED
@@ -670,10 +670,10 @@ class Lifecycle extends EventTarget {
670
670
  return new Promise((resolve, reject) => {
671
671
  const FCID = getFCID();
672
672
  const logger = sdkLogger.withFields({ FCID });
673
- logger.log("lifecycle moveToBackground: sending play action");
674
673
  const configuration = remotePlayer.getConfiguration();
675
674
  const audioLanguage = remotePlayer._selectedAudioTrack || configuration.preferredAudioLanguage || "";
676
- const subtitlesLanguage = remotePlayer.textTrackVisibility && (remotePlayer._selectedSubtitlesTrack || configuration.preferredSubtitlesLanguage) || "";
675
+ const subtitlesLanguage = remotePlayer._selectedSubtitlesTrack || configuration.preferredSubtitlesLanguage || "";
676
+
677
677
  let request;
678
678
  const message = {
679
679
  action: "play",
@@ -685,7 +685,7 @@ class Lifecycle extends EventTarget {
685
685
  message.type = "remotePlayer.play";
686
686
  message.class = "remotePlayer";
687
687
  message.switchMode = remotePlayer._isAudioSyncEnabled() ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS;
688
- message.streamType = StreamType.AUDIO | StreamType.VIDEO | StreamType.SUBTITLE;
688
+ message.streamType = remotePlayer.textTrackVisibility ? (StreamType.AUDIO | StreamType.VIDEO | StreamType.SUBTITLE) : (StreamType.AUDIO | StreamType.VIDEO);
689
689
  request = {
690
690
  target: "TC",
691
691
  waitForResponse: true,
@@ -693,8 +693,12 @@ class Lifecycle extends EventTarget {
693
693
  message: JSON.stringify(message)
694
694
  };
695
695
  } else {
696
+ if (!remotePlayer.textTrackVisibility) {
697
+ message.subtitlesLanguage = "";
698
+ }
696
699
  request = message;
697
700
  }
701
+ logger.log(`lifecycle moveToBackground: sending play action audioLanguage=${message.audioLanguage} subtitlesLanguage=${message.subtitlesLanguage} textTrackVisibility=${remotePlayer.textTrackVisibility}`);
698
702
  let timerId = 0;
699
703
  const timeBeforeSendingRequest = Date.now();
700
704
  const queryId = window.cefQuery({
@@ -171,6 +171,15 @@ class RemotePlayer extends EventTarget {
171
171
  * });
172
172
  * */
173
173
 
174
+ /**
175
+ * @event RemotePlayer#playing
176
+ * @description playing event will be dispatched when the remote player started playback of the first audio frame. This event can be used to synchronize the local player with the remote player.
177
+ * @example
178
+ * remotePlayer.addEventListener("playing", () => {
179
+ * console.info("remotePlayer playing");
180
+ * });
181
+ * */
182
+
174
183
  /**
175
184
  * @event RemotePlayer#loadedmetadata
176
185
  * @description loadedmetadata event will be dispatched when the remote player metadata is loaded
@@ -495,13 +504,7 @@ class RemotePlayer extends EventTarget {
495
504
  }
496
505
  if (this._availableTextTracks) {
497
506
  const selectedTrack = this._availableTextTracks.find((track) => track.selected === true);
498
- if (selectedTrack) {
499
- this._selectedSubtitlesTrack = selectedTrack.id;
500
- this._textTrackVisibility = true;
501
- } else {
502
- this._selectedSubtitlesTrack = "";
503
- this._textTrackVisibility = false;
504
- }
507
+ this._selectedSubtitlesTrack = selectedTrack?.id || "";
505
508
  }
506
509
  }
507
510
 
@@ -698,12 +701,14 @@ class RemotePlayer extends EventTarget {
698
701
  if (window.cefQuery) {
699
702
  const FCID = getFCID();
700
703
  const logger = sdkLogger.withFields({ FCID });
701
- logger.log("remotePlayer play: sending play action");
702
704
  const audioLanguage = this._selectedAudioTrack || this._config.preferredAudioLanguage || "";
703
- let subtitlesLanguage = "";
704
- if (this._textTrackVisibility) {
705
- subtitlesLanguage = this._selectedSubtitlesTrack || this._config.preferredSubtitlesLanguage || "";
705
+ const subtitlesLanguage = this._selectedSubtitlesTrack || this._config.preferredSubtitlesLanguage || "";
706
+
707
+ if (this._remotePlayerApiVersion >= 2 && !this._textTrackVisibility && streamType === StreamType.SUBTITLE) {
708
+ logger.log("remotePlayer play: text track visibility is disabled and streamType is only SUBTITLE. returning early with no action.");
709
+ return Promise.resolve(undefined); // nothing to do
706
710
  }
711
+
707
712
  const message = {
708
713
  type: "remotePlayer.play",
709
714
  class: "remotePlayer",
@@ -718,7 +723,16 @@ class RemotePlayer extends EventTarget {
718
723
  message.switchMode = this._isAudioSyncEnabled() ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS;
719
724
  message.streamType = streamType;
720
725
  waitForResponse = true;
726
+
727
+ if (!this._textTrackVisibility && (message.streamType & StreamType.SUBTITLE) !== 0) {
728
+ // remove SUBTITLE
729
+ message.streamType = message.streamType & ~StreamType.SUBTITLE;
730
+ logger.log("remotePlayer play: text track visibility is disabled. Removed SUBTITLE from streamType.");
731
+ }
732
+ } else if (!this.textTrackVisibility) {
733
+ message.subtitlesLanguage = "";
721
734
  }
735
+ logger.log(`remotePlayer play: sending play action remotePlayer._isPlaying: ${this._isPlaying} audioLanguage=${message.audioLanguage} subtitlesLanguage=${message.subtitlesLanguage} textTrackVisibility=${this.textTrackVisibility}`);
722
736
  const request = { target: "TC", waitForResponse: waitForResponse, message: JSON.stringify(message) };
723
737
  return new Promise((resolve, reject) => {
724
738
  let timerId = 0;
@@ -947,10 +961,7 @@ class RemotePlayer extends EventTarget {
947
961
  const playbackPosition = position ?? 0;
948
962
  const logger = sdkLogger.withFields({ FCID, loadUrl: url, playbackPosition });
949
963
  const audioLanguage = audioTrackId || this._selectedAudioTrack || this._config.preferredAudioLanguage || "";
950
- let subtitlesLanguage = "";
951
- if (this._textTrackVisibility) {
952
- subtitlesLanguage = textTrackId || this._selectedSubtitlesTrack || this._config.preferredSubtitlesLanguage || "";
953
- }
964
+ const subtitlesLanguage = textTrackId || this._selectedSubtitlesTrack || this._config.preferredSubtitlesLanguage || "";
954
965
 
955
966
  const message = {
956
967
  url,
@@ -968,7 +979,7 @@ class RemotePlayer extends EventTarget {
968
979
  } else {
969
980
  message.type = "setPlayableUri";
970
981
  }
971
- logger.log(`remotePlayer load: sending ${message.type} request. remotePlayer._isPlaying: ${this._isPlaying}`);
982
+ logger.log(`remotePlayer load: sending ${message.type} request. remotePlayer._isPlaying: ${this._isPlaying} audioLanguage=${audioLanguage} subtitlesLanguage=${subtitlesLanguage}`);
972
983
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
973
984
  let timerId = 0;
974
985
  const timeBeforeSendingRequest = Date.now();
@@ -1519,20 +1530,23 @@ class RemotePlayer extends EventTarget {
1519
1530
  /**
1520
1531
  * Enable or disable the subtitles.
1521
1532
  * If the player is in an unloaded state, the request will be applied next time content is played.
1522
- * @param {boolean} visible whether the subtitles are visible or not
1523
- * @throws {TypeError} if visible is not a boolean variable
1533
+ * @param {boolean|0|1} visible whether the subtitles are visible or not
1534
+ * @throws {TypeError} if visible is not a boolean variable or 0/1
1524
1535
  */
1525
1536
  setTextTrackVisibility(visible) {
1526
1537
  const oldVisibility = this._textTrackVisibility;
1527
- if (typeof visible !== "boolean") {
1528
- throw new TypeError("visible parameter must be a boolean");
1538
+ if (typeof visible !== "boolean" && !(visible === 0 || visible === 1)) {
1539
+ throw new TypeError("visible parameter must be a boolean or 0/1");
1529
1540
  }
1530
- const newVisibility = visible;
1531
- if (newVisibility === oldVisibility) {
1541
+ // Convert to boolean in case apps pass 0/1 instead false/true.
1542
+ const newVisibility = !!visible;
1543
+
1544
+ if (oldVisibility === newVisibility) {
1532
1545
  return;
1533
1546
  }
1547
+
1534
1548
  this._textTrackVisibility = newVisibility;
1535
- if (!newVisibility) {
1549
+ if (!this._textTrackVisibility) {
1536
1550
  // Setting the visibility to false clears any previous selections user has done
1537
1551
  this._selectedSubtitlesTrack = "";
1538
1552
  }
@@ -55,35 +55,92 @@ const senzaShaka = { ...shaka };
55
55
  */
56
56
 
57
57
  export class SenzaShakaPlayer extends shaka.Player {
58
+ /** @private {SenzaShakaPlayer|null} Previous instance of the player */
58
59
  static _prevInstance = null;
60
+
59
61
  /**
60
62
  * @private
61
63
  * @type {Object.<string, string>}
62
- * @description Map between audio track language and id
64
+ * @description Map of audio track languages to their IDs
63
65
  */
64
66
  _audioTracksMap = {};
65
67
 
66
68
  /**
67
69
  * @private
68
70
  * @type {Object.<string, string>}
69
- * @description Map between text track language and id
71
+ * @description Map of text track languages to their IDs
70
72
  */
71
73
  _textTracksMap = {};
72
74
 
75
+ /**
76
+ * @private
77
+ * @type {number}
78
+ * @description Timeout in milliseconds to wait for playing event
79
+ * @default 3000
80
+ */
81
+ _playingTimeout = 3000;
82
+
83
+ /**
84
+ * @private
85
+ * @type {number|null}
86
+ * @description Timestamp when play was called
87
+ */
88
+ _playStartTime = null;
89
+
90
+ /**
91
+ * @private
92
+ * @type {boolean}
93
+ * @description Whether to stop remote player on error
94
+ * @default false
95
+ */
96
+ _shouldStopRemotePlayerOnError = false;
97
+
98
+ /**
99
+ * @private
100
+ * @type {Function|null}
101
+ * @description Original play function from video element
102
+ */
103
+ _originalPlay = null;
104
+
105
+ /**
106
+ * @private
107
+ * @type {Function|null}
108
+ * @description Resolve function for play promise
109
+ */
110
+ _playPromiseResolve = null;
111
+
112
+ /**
113
+ * @private
114
+ * @type {Function|null}
115
+ * @description Reject function for play promise
116
+ */
117
+ _playPromiseReject = null;
118
+
119
+ /**
120
+ * @private
121
+ * @type {number|null}
122
+ * @description Timer ID for play timeout
123
+ */
124
+ _playTimeoutId = null;
125
+
73
126
  /**
74
127
  * @private
75
128
  * @type {Object.<string, Function>}
76
- * @description Object containing event listeners for the video element.
129
+ * @description Event listeners for video element
77
130
  */
78
131
  _videoEventListeners = {
79
132
  "play": () => {
80
- this.remotePlayer.play()
81
- .catch(error => {
82
- sdkLogger.error("Failed to play remote player:", error);
83
- this.handleSenzaError(error.code, error.message || "Unknown play error");
84
- });
133
+ // keep old play behavior, in case playing timeout is defined as 0
134
+ if (this._playingTimeout === 0) {
135
+ this.remotePlayer.play()
136
+ .catch(error => {
137
+ sdkLogger.error("Failed to play remote player:", error);
138
+ this.handleSenzaError(error.code, error.message || "Unknown play error");
139
+ });
140
+ }
85
141
  },
86
- "pause": () => {
142
+ "pause" : () => {
143
+ this._resetPlayPromise();
87
144
  this.remotePlayer.pause()
88
145
  .catch(error => {
89
146
  sdkLogger.error("Failed to pause remote player:", error);
@@ -97,7 +154,7 @@ export class SenzaShakaPlayer extends shaka.Player {
97
154
  /**
98
155
  * @private
99
156
  * @type {Object.<string, Function>}
100
- * @description Object containing event listeners for the remote player.
157
+ * @description Event listeners for remote player
101
158
  */
102
159
  _remotePlayerEventListeners = {
103
160
  "ended": () => {
@@ -109,7 +166,6 @@ export class SenzaShakaPlayer extends shaka.Player {
109
166
  },
110
167
  "error": (event) => {
111
168
  sdkLogger.log("remotePlayer error:", event.detail.errorCode, event.detail.message);
112
- // we need to move to foreground here for the auto background mode to work
113
169
  lifecycle.moveToForeground();
114
170
  this.handleSenzaError(event.detail.errorCode, event.detail.message);
115
171
  },
@@ -166,17 +222,85 @@ export class SenzaShakaPlayer extends shaka.Player {
166
222
  this._textTracksMap[lang] = track.id;
167
223
  }
168
224
  }
225
+ },
226
+ "playing": async () => {
227
+ sdkLogger.info("remotePlayer playing event received");
228
+ // If playing timeout was not expored, and the feature is set, handle the playing event
229
+ if (this._playingTimeout > 0 && this._playTimeoutId !== null) {
230
+ this._handlePlayingEvent();
231
+ }
169
232
  }
170
233
  };
171
234
 
235
+ /**
236
+ * Clears the play timeout and resets the timer ID
237
+ * @private
238
+ * @description Cancels any pending play timeout and nullifies the timeout ID
239
+ */
240
+ _clearPlayTimeout = () => {
241
+ if (this._playTimeoutId) {
242
+ sdkLogger.info("Clearing play timeout");
243
+ clearTimeout(this._playTimeoutId);
244
+ this._playTimeoutId = null;
245
+ }
246
+ };
247
+ /**
248
+ * @private
249
+ * @description Handles the playing event from the remote player.
250
+ */
251
+ _handlePlayingEvent = async () => {
172
252
 
253
+ this._clearPlayTimeout();
254
+
255
+ if (this.videoElement && this._originalPlay && this._playPromiseResolve) {
256
+ try {
257
+ const elapsedTime = Date.now() - this._playStartTime;
258
+ sdkLogger.info(`Time for playback start ${elapsedTime}ms`);
259
+ await this._originalPlay.call(this.videoElement);
260
+ sdkLogger.info("Video element play resolved successfully");
261
+ this._playPromiseResolve();
262
+ } catch (error) {
263
+ this._handlePlayPromiseError(error);
264
+ return;
265
+ } finally {
266
+ this._playPromiseResolve = null;
267
+ this._playPromiseReject = null;
268
+ }
269
+ }
270
+ };
173
271
  /**
174
- * Flag indicating whether the remote player should be stopped when an error occurs
175
272
  * @private
176
- * @type {boolean}
177
- * @default false
273
+ * @description Resets the play promise state, clearing any existing resolve/reject functions and timeouts.
178
274
  */
179
- _shouldStopRemotePlayerOnError = false;
275
+ _resetPlayPromise = () => {
276
+ if (this._playPromiseResolve) {
277
+ sdkLogger.info("Resolving play promise");
278
+ this._playPromiseResolve();
279
+ }
280
+ this._playPromiseResolve = null;
281
+ this._playPromiseReject = null;
282
+ this._clearPlayTimeout();
283
+ };
284
+
285
+
286
+ /**
287
+ * Handles errors for play promises and remote player events.
288
+ * @private
289
+ * @param {Error} error - The error object.
290
+ */
291
+ _handlePlayPromiseError(error) {
292
+
293
+ sdkLogger.error("Error while waiting for playing event:", error);
294
+ if (this._playPromiseReject) {
295
+ this._playPromiseReject(error);
296
+ this._playPromiseResolve = null;
297
+ this._playPromiseReject = null;
298
+ }
299
+ if (this._playTimeoutId) {
300
+ clearTimeout(this._playTimeoutId);
301
+ this._playTimeoutId = null;
302
+ }
303
+ }
180
304
 
181
305
  /**
182
306
  * Creates an instance of SenzaShakaPlayer, which is a subclass of shaka.Player.
@@ -201,15 +325,62 @@ export class SenzaShakaPlayer extends shaka.Player {
201
325
 
202
326
  this.remotePlayer = remotePlayer;
203
327
  this._addRemotePlayerEventListeners();
328
+ SenzaShakaPlayer._prevInstance = this;
329
+ const playTimeout = getPlatformInfo()?.sessionInfo?.settings?.["ui-streamer"]?.playingEventTimeout;
330
+ this._playingTimeout = (playTimeout >= 0) ? playTimeout*1000 : this._playingTimeout;
331
+
204
332
  // if video element is provided, add the listeres here. In this case ,there is no need to call attach.
205
333
  if (videoElement) {
206
- this.videoElement = videoElement;
207
- this._attachVideoElementToRemotePlayer();
334
+ this._attach(videoElement);
208
335
  sdkLogger.warn("SenzaShakaPlayer constructor Adding videoElement in the constructor is going to be deprecated in the future. Please use attach method instead.");
209
336
  }
210
- SenzaShakaPlayer._prevInstance = this;
211
337
 
338
+ }
339
+
340
+ _attach(videoElement) {
341
+ this.videoElement = videoElement;
342
+
343
+ // Store original play and replace with our implementation.
344
+ if (this._playingTimeout > 0) {
345
+ this._originalPlay = this.videoElement.play;
346
+ this.videoElement.play = async () => {
347
+ if (this._playPromiseResolve || this._playPromiseReject) {
348
+ sdkLogger.warn("play() was called while a play promise is already pending. Ignoring play call.");
349
+ return;
350
+ }
351
+
352
+ // Clear any existing timeout
353
+ if (this._playTimeoutId) {
354
+ clearTimeout(this._playTimeoutId);
355
+ }
356
+ // Store the timestamp of the play call
357
+ this._playStartTime = Date.now();
358
+
359
+ const returnPromise = new Promise((resolve, reject) => {
360
+ this._playPromiseResolve = resolve;
361
+ this._playPromiseReject = reject;
362
+
363
+
364
+ });
365
+ // Set timeout to reject if playing event doesn't arrive
366
+ this._playTimeoutId = setTimeout(() => {
367
+ sdkLogger.error("Playing event timeout reached");
368
+ // when timeout reached start the local playback anyway
369
+ this._handlePlayingEvent();
370
+ }, this._playingTimeout);
371
+ await this.remotePlayer.play()
372
+ .catch(error => {
373
+ sdkLogger.error("Failed to play remote player:", error);
374
+ this.handleSenzaError(error.code, error.message || "Unknown play error");
375
+
376
+ });
377
+
378
+ // Create a new promise that will resolve when the real play succeeds
379
+ return returnPromise;
380
+ };
381
+ };
212
382
 
383
+ this._attachVideoElementToRemotePlayer();
213
384
  }
214
385
 
215
386
  /**
@@ -220,8 +391,7 @@ export class SenzaShakaPlayer extends shaka.Player {
220
391
  */
221
392
  async attach(videoElement, initializeMediaSource = true) {
222
393
  await super.attach(videoElement, initializeMediaSource);
223
- this.videoElement = videoElement;
224
- this._attachVideoElementToRemotePlayer();
394
+ this._attach(videoElement);
225
395
  }
226
396
 
227
397
  /**
@@ -235,6 +405,17 @@ export class SenzaShakaPlayer extends shaka.Player {
235
405
  * @export
236
406
  */
237
407
  async detach(keepAdManager = false) {
408
+ // Clear any pending timeout
409
+ this._resetPlayPromise();
410
+
411
+ if (this.videoElement && this._originalPlay) {
412
+ // Clear any pending play promise
413
+ this._playPromiseResolve = null;
414
+ this._playPromiseReject = null;
415
+ // Restore original play function before detaching
416
+ this.videoElement.play = this._originalPlay;
417
+ this._originalPlay = null;
418
+ }
238
419
 
239
420
  await super.detach(keepAdManager);
240
421
  this._audioTracksMap = {};
@@ -265,6 +446,7 @@ export class SenzaShakaPlayer extends shaka.Player {
265
446
  // Call the remote player's unload method
266
447
  try {
267
448
  await lifecycle.moveToForeground();
449
+ this._clearPlayTimeout();
268
450
  await remotePlayer.unload();
269
451
  } catch (error) {
270
452
  sdkLogger.error("Failed to unload remote player:", error);
@@ -391,7 +573,8 @@ export class SenzaShakaPlayer extends shaka.Player {
391
573
  sdkLogger.error("Error while trying to stop video element playback:", stopError);
392
574
  }
393
575
  }
394
-
576
+ // Handle error while waiting for play event
577
+ this._handlePlayPromiseError(error);
395
578
  this.dispatchEvent(new shaka.util.FakeEvent("error", errorMap));
396
579
  }
397
580
 
@@ -487,6 +670,17 @@ export class SenzaShakaPlayer extends shaka.Player {
487
670
  return super.destroy();
488
671
  }
489
672
 
673
+ /**
674
+ * A temporary override for older versions of Shaka.
675
+ * Senza doesn't support out-of-band subtitles
676
+ */
677
+ addTextTrack(uri, language, kind, mimeType, codec, label, forced = false) {
678
+ sdkLogger.warn("addTextTrack is deprecated, please use addTextTrackAsync");
679
+ super.addTextTrackAsync(uri, language, kind, mimeType, codec, label, forced).then(subs => {
680
+ sdkLogger.warn("addTextTrackAsync Done" + subs);
681
+ });
682
+ }
683
+
490
684
  /**
491
685
  * Override the configure method to add custom configuration handling
492
686
  * Supports the following additional configuration options:
@@ -502,6 +696,8 @@ export class SenzaShakaPlayer extends shaka.Player {
502
696
  * });
503
697
  */
504
698
  configure(config) {
699
+ sdkLogger.log("configure player with: ", JSON.stringify(config));
700
+
505
701
  // Handle custom configuration
506
702
  if (config.shouldStopRemotePlayerOnError !== undefined) {
507
703
  this._shouldStopRemotePlayerOnError = !!config.shouldStopRemotePlayerOnError;
@@ -522,6 +718,7 @@ export class SenzaShakaPlayer extends shaka.Player {
522
718
  remoteConfiguration["preferredSubtitlesLanguage"] = config["preferredTextLanguage"];
523
719
  }
524
720
 
721
+ sdkLogger.log("configure remote player with: ", JSON.stringify(remoteConfiguration));
525
722
  remotePlayer.configure(remoteConfiguration);
526
723
  }
527
724