senza-sdk 4.4.0 → 4.4.1-4ca75a1.0

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.
@@ -10,6 +10,31 @@
10
10
  SPDX-License-Identifier: Apache-2.0
11
11
  */
12
12
 
13
+ /*
14
+ @license
15
+ Copyright 2013 Ali Al Dallal
16
+
17
+ Licensed under the MIT license.
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ of this software and associated documentation files (the "Software"), to deal
21
+ in the Software without restriction, including without limitation the rights
22
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
+ copies of the Software, and to permit persons to whom the Software is
24
+ furnished to do so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in
27
+ all copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
+ SOFTWARE.
36
+ */
37
+
13
38
  /*
14
39
  @license
15
40
  EME Encryption Scheme Polyfill
@@ -45,6 +70,13 @@
45
70
  SPDX-License-Identifier: Apache-2.0
46
71
  */
47
72
 
73
+ /*
74
+ @license
75
+ glMatrix: https://github.com/toji/gl-matrix/
76
+ Copyright 2015-2021, Brandon Jones, Colin MacKenzie IV
77
+ SPDX-License-Identifier: MIT
78
+ */
79
+
48
80
  /*
49
81
  @license
50
82
  tXml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senza-sdk",
3
- "version": "4.4.0",
3
+ "version": "4.4.1-4ca75a1.0",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
@@ -130,7 +130,7 @@ export async function init(interfaceApiVersion, showSequenceFunc, initSequenceFu
130
130
 
131
131
  // Initialize lifecycle first to make sure the state is updated.
132
132
  await lifecycle._init(sessionInfoObj?.settings?.["ui-streamer"], triggerEvent);
133
- await remotePlayer._init(sessionInfoObj?.settings?.["ui-streamer"], triggerEvent);
133
+ await remotePlayer._init(sessionInfoObj, triggerEvent);
134
134
  alarmManager._init();
135
135
  messageManager._init();
136
136
  sdkLogger.log("All submodules initialized");
@@ -29,7 +29,9 @@ class Lifecycle extends LifecycleInterface {
29
29
  * @private
30
30
  */
31
31
  this._isInitialized = false;
32
- this._inTransition = false;
32
+ this._inTransitionToForeground = false;
33
+ this._inTransitionToBackground = false;
34
+ this._inTransitionToStandby = false;
33
35
 
34
36
  /**
35
37
  * Event listeners manager for the userdisconnected event
@@ -310,7 +312,7 @@ class Lifecycle extends LifecycleInterface {
310
312
  // This api is part of epic HSDEV-713
311
313
  _moveToUiStandby() {
312
314
  if (window.cefQuery) {
313
- this._inTransition = true;
315
+ this._inTransitionToStandby = true;
314
316
  return new Promise((resolve, reject) => {
315
317
  const FCID = getFCID();
316
318
  const request = { target: "TC", waitForResponse: false, internalAction: "uiExit", message: JSON.stringify({ type: "uiStandbyRequest", fcid: FCID }) };
@@ -321,12 +323,12 @@ class Lifecycle extends LifecycleInterface {
321
323
  persistent: false,
322
324
  onSuccess: () => {
323
325
  logger.log("[ moveToUiStandby ] moveToUiStandby successfully sent");
324
- this._inTransition = false;
326
+ this._inTransitionToStandby = false;
325
327
  resolve(true);
326
328
  },
327
329
  onFailure: (code, msg) => {
328
330
  logger.error(`[ moveToUiStandby ] moveToUiStandby failed: ${code} ${msg}`);
329
- this._inTransition = false;
331
+ this._inTransitionToStandby = false;
330
332
  reject(`moveToUiStandby failed: ${code} ${msg}`);
331
333
  }
332
334
  });
@@ -423,6 +425,14 @@ class Lifecycle extends LifecycleInterface {
423
425
  this._countdown = null;
424
426
  }
425
427
 
428
+ /**
429
+ * @private
430
+ */
431
+ _isInTransition() {
432
+ return this._inTransitionToForeground || this._inTransitionToBackground || this._inTransitionToStandby;
433
+ }
434
+
435
+
426
436
  getState() {
427
437
  if (window.cefQuery) {
428
438
  return new Promise((resolve, reject) => {
@@ -445,14 +455,19 @@ class Lifecycle extends LifecycleInterface {
445
455
 
446
456
  moveToForeground() {
447
457
  if (window.cefQuery) {
448
- if (this._inTransition || this._state === this.UiState.FOREGROUND || this._state === this.UiState.IN_TRANSITION_TO_FOREGROUND) {
449
- sdkLogger.warn(`lifecycle moveToForeground: No need to transition to foreground, state: ${this._state} transition: ${this._inTransition}`);
458
+ const inTransition = this._isInTransition();
459
+ if (inTransition || this._state === this.UiState.FOREGROUND || this._state === this.UiState.IN_TRANSITION_TO_FOREGROUND) {
460
+ sdkLogger.warn(`lifecycle moveToForeground: No need to transition to foreground, state: ${this._state} transition: ${inTransition}`);
450
461
  return Promise.resolve(false);
451
462
  }
452
- this._inTransition = true;
463
+ this._inTransitionToForeground = true;
453
464
  alarmManager._moveToForegroundCalled();
454
465
  const FCID = getFCID();
455
466
  if (this._remotePlayerApiVersion >= 2) {
467
+ // Only update to playing UI if we started seeking in ABR. But, if we are seeking while already paused, keep the target seek state as is.
468
+ if (remotePlayer._isSeekingByApplication && remotePlayer._targetSeekPlayingState === TargetPlayingState.PLAYING_ABR) {
469
+ remotePlayer._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
470
+ }
456
471
  return new Promise((resolve, reject) => {
457
472
  const FCID = getFCID();
458
473
  const logger = sdkLogger.withFields({ FCID });
@@ -472,14 +487,14 @@ class Lifecycle extends LifecycleInterface {
472
487
  onSuccess: () => {
473
488
  const duration = Date.now() - timeBeforeSendingRequest;
474
489
  logger.withFields({ duration }).log(`stop completed successfully after ${duration} ms`);
475
- this._inTransition = false;
490
+ this._inTransitionToForeground = false;
476
491
  timerId = clearTimer(timerId);
477
492
  resolve(true);
478
493
  },
479
494
  onFailure: (code, msg) => {
480
495
  const duration = Date.now() - timeBeforeSendingRequest;
481
496
  logger.withFields({ duration }).log(`stop failed after ${duration} ms. Error code: ${code}, error message: ${msg}`);
482
- this._inTransition = false;
497
+ this._inTransitionToForeground = false;
483
498
  timerId = clearTimer(timerId);
484
499
  reject(new SenzaError(code, msg));
485
500
  }
@@ -488,7 +503,7 @@ class Lifecycle extends LifecycleInterface {
488
503
  const timeout = this._remotePlayerConfirmationTimeout + 1000;
489
504
  timerId = setTimeout(() => {
490
505
  logger.log(`stop reached timeout of ${timeout} ms, canceling query id ${queryId}`);
491
- this._inTransition = false;
506
+ this._inTransitionToForeground = false;
492
507
  window.cefQueryCancel(queryId);
493
508
  reject(new SenzaError(6000, `stop reached timeout of ${timeout} ms`));
494
509
  }, timeout, queryId);
@@ -503,11 +518,11 @@ class Lifecycle extends LifecycleInterface {
503
518
  persistent: false,
504
519
  onSuccess: () => {
505
520
  logger.log("uiActiveRequest successfully sent");
506
- this._inTransition = false;
521
+ this._inTransitionToForeground = false;
507
522
  resolve(true);
508
523
  },
509
524
  onFailure: (code, msg) => {
510
- this._inTransition = false;
525
+ this._inTransitionToForeground = false;
511
526
  logger.error(`uiActiveRequest failed: ${code} ${msg}`);
512
527
  reject(`uiActiveRequest failed: ${code} ${msg}`);
513
528
  }
@@ -520,10 +535,6 @@ class Lifecycle extends LifecycleInterface {
520
535
 
521
536
  _moveToBackground() {
522
537
  if (window.cefQuery) {
523
- if (this._inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
524
- sdkLogger.warn(`lifecycle moveToBackground: No need to transition to background, state: ${this._state} transition: ${this._inTransition}`);
525
- return Promise.resolve(false);
526
- }
527
538
  // If audio sync is disabled, we only need to sync before remote player starts playing
528
539
  if (!isAudioSyncConfigured()) {
529
540
  remotePlayer._syncRemotePlayerWithLocalPlayer();
@@ -534,8 +545,8 @@ class Lifecycle extends LifecycleInterface {
534
545
  if (!remotePlayer._isPlaying) {
535
546
  return this._moveToUiStandby();
536
547
  }
537
-
538
- this._inTransition = true;
548
+ remotePlayer._changePlayMode(true);
549
+ this._inTransitionToBackground = true;
539
550
  return new Promise((resolve, reject) => {
540
551
  const FCID = getFCID();
541
552
  const logger = sdkLogger.withFields({ FCID });
@@ -553,7 +564,8 @@ class Lifecycle extends LifecycleInterface {
553
564
  if (this._remotePlayerApiVersion >= 2) {
554
565
  message.type = "remotePlayer.play";
555
566
  message.class = "remotePlayer";
556
- message.switchMode = remotePlayer._isAudioSyncEnabled() ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS;
567
+ // in background, client expects to get NON_SEAMLESS switch mode.
568
+ message.switchMode = remotePlayer._isAudioSyncEnabled() && this._state !== this.UiState.BACKGROUND ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS;
557
569
  message.streamType = remotePlayer.textTrackVisibility ? (StreamType.AUDIO | StreamType.VIDEO | StreamType.SUBTITLE) : (StreamType.AUDIO | StreamType.VIDEO);
558
570
  request = {
559
571
  target: "TC",
@@ -576,14 +588,14 @@ class Lifecycle extends LifecycleInterface {
576
588
  onSuccess: () => {
577
589
  const duration = Date.now() - timeBeforeSendingRequest;
578
590
  logger.withFields({ duration }).log(`play completed successfully after ${duration} ms`);
579
- this._inTransition = false;
591
+ this._inTransitionToBackground = false;
580
592
  timerId = clearTimer(timerId);
581
593
  resolve();
582
594
  },
583
595
  onFailure: (code, msg) => {
584
596
  const duration = Date.now() - timeBeforeSendingRequest;
585
597
  logger.withFields({ duration }).log(`play failed after ${duration} ms. Error code: ${code}, error message: ${msg}`);
586
- this._inTransition = false;
598
+ this._inTransitionToBackground = false;
587
599
  timerId = clearTimer(timerId);
588
600
  reject(new SenzaError(code, msg));
589
601
  }
@@ -593,7 +605,7 @@ class Lifecycle extends LifecycleInterface {
593
605
  const timeout = this._remotePlayerConfirmationTimeout + 1000;
594
606
  timerId = setTimeout(() => {
595
607
  logger.log(`play reached timeout of ${timeout} ms, canceling query id ${queryId}`);
596
- this._inTransition = false;
608
+ this._inTransitionToBackground = false;
597
609
  window.cefQueryCancel(queryId);
598
610
  reject(new SenzaError(6000, `play reached timeout of ${timeout} ms`));
599
611
  }, timeout, queryId);
@@ -606,11 +618,11 @@ class Lifecycle extends LifecycleInterface {
606
618
 
607
619
  moveToBackground() {
608
620
  if (window.cefQuery) {
609
- if (this._inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
610
- sdkLogger.warn(`lifecycle moveToBackground: No need to transition to background, state: ${this._state} transition: ${this._inTransition}`);
621
+ const inTransition = this._isInTransition();
622
+ if (inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
623
+ sdkLogger.warn(`lifecycle moveToBackground: No need to transition to background, state: ${this._state} transition: ${inTransition}`);
611
624
  return Promise.resolve(false);
612
625
  }
613
-
614
626
  if (remotePlayer._isSeekingByApplication) {
615
627
  remotePlayer._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
616
628
  return Promise.resolve(true);
@@ -11,7 +11,8 @@ import {
11
11
  SeekState,
12
12
  TargetPlayingState,
13
13
  isSubtitlesTranslationAllowed,
14
- isSubtitlesTranslationPattern
14
+ isSubtitlesTranslationPattern,
15
+ isAppVersionAboveOrEqual
15
16
  } from "./utils";
16
17
  import { lifecycle } from "./lifecycle";
17
18
  import { writeLicenseResponse } from "./api";
@@ -143,10 +144,12 @@ class RemotePlayer extends RemotePlayerInterface {
143
144
  }
144
145
 
145
146
  /** @private Initialize the remote player
146
- * @param {Object} uiStreamerSettings ui-streamer portion of the settings taken from session info
147
+ * @param {Object} sessionObj session information object
148
+ * @param {Object} triggerEvent trigger event object
147
149
  * */
148
- async _init(uiStreamerSettings, triggerEvent) {
150
+ async _init(sessionObj, triggerEvent) {
149
151
  sdkLogger.info("Initializing RemotePlayer");
152
+ const uiStreamerSettings = sessionObj?.settings?.["ui-streamer"];
150
153
  let playerState = {
151
154
  isLoaded: false,
152
155
  playbackUrl: ""
@@ -193,6 +196,7 @@ class RemotePlayer extends RemotePlayerInterface {
193
196
  this._remotePlayerConfirmationTimeout = uiStreamerSettings?.remotePlayerConfirmationTimeout ?? DEFAULT_REMOTE_PLAYER_CONFIRMATION_TIMEOUT;
194
197
  this._remotePlayerApiVersion = uiStreamerSettings?.remotePlayerApiVersion || 1;
195
198
  this._multiSeekDelay = uiStreamerSettings?.multiSeekDelay || MULTI_SEEK_DELAY_MSEC;
199
+ this._deviceAppVersion = sessionObj?.manifest?.["device-app"] || "";
196
200
 
197
201
  sdkLogger.info(`remotePLayer isPlaying=${this._isPlaying}`);
198
202
 
@@ -527,7 +531,8 @@ class RemotePlayer extends RemotePlayerInterface {
527
531
  };
528
532
  let waitForResponse = false;
529
533
  if (this._remotePlayerApiVersion >= 2) {
530
- message.switchMode = this._isAudioSyncEnabled() ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS;
534
+ // in background, client expects to get NON_SEAMLESS switch mode.
535
+ message.switchMode = this._isAudioSyncEnabled() && lifecycle.state !== lifecycle.UiState.BACKGROUND ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS;
531
536
  message.streamType = streamType;
532
537
  waitForResponse = true;
533
538
 
@@ -948,8 +953,13 @@ class RemotePlayer extends RemotePlayerInterface {
948
953
 
949
954
  // If seeking in progress, wait for seek to complete before playing
950
955
  if (this._isSeekingByApplication) {
951
- sdkLogger.info("application requesting play during seek");
952
- this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
956
+ if (this._inTransitionToForeground || lifecycle.state === lifecycle.UiState.FOREGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND) {
957
+ sdkLogger.info("application requesting play during seek. setting targetSeekPlayingState to PLAYING_UI");
958
+ this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
959
+ } else {
960
+ sdkLogger.info("application requesting play during seek. setting targetSeekPlayingState to PLAYING_ABR");
961
+ this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
962
+ }
953
963
  return Promise.resolve(true);
954
964
  }
955
965
  /*
@@ -1510,6 +1520,8 @@ class RemotePlayer extends RemotePlayerInterface {
1510
1520
  * @private
1511
1521
  */
1512
1522
  async _startSeeking(playbackPosition) {
1523
+ const backgroundSeekMinClientAppVersion = "25.27.8";
1524
+
1513
1525
  if (this._isSetAudioInProgress || this._isSetSubtitlesInProgress) {
1514
1526
  sdkLogger.info("Seeking not supported while setAudioLanguage or setSubtitleLanguage are in progress.");
1515
1527
  return;
@@ -1525,10 +1537,12 @@ class RemotePlayer extends RemotePlayerInterface {
1525
1537
  return;
1526
1538
  }
1527
1539
  }
1528
-
1529
- // Only allow seeking in foreground. Still ignore the initialized local player seeking event above
1530
- if (this._remotePlayerApiVersion >= 2 && !this._isSeekingByPlatform && !this._isSeekingByApplication &&
1531
- (lifecycle.state === lifecycle.UiState.FOREGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND)) {
1540
+ const backgroundSeekSupported = isAppVersionAboveOrEqual(this._deviceAppVersion, backgroundSeekMinClientAppVersion);
1541
+ if (!backgroundSeekSupported && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
1542
+ sdkLogger.warn(`Seeking: background seek not supported for device app version: ${this._deviceAppVersion}`);
1543
+ }
1544
+ if (this._remotePlayerApiVersion >= 2 && !this._isSeekingByPlatform && !this._isSeekingByApplication
1545
+ && (lifecycle.state === lifecycle.UiState.FOREGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND || backgroundSeekSupported)) {
1532
1546
  this._atomicSeek();
1533
1547
  } else {
1534
1548
  sdkLogger.info(`Seeking: skipping seeking event to currentTime: ${playbackPosition}, internalSeek: ${this._isSeekingByPlatform}, localPlayerSeek: ${this._isSeekingByApplication}, state: ${lifecycle.state}`);
@@ -1547,16 +1561,16 @@ class RemotePlayer extends RemotePlayerInterface {
1547
1561
  * */
1548
1562
  async _atomicSeek() {
1549
1563
  sdkLogger.info("Seeking: local video element seeking start while isPlaying: ", this._isPlaying);
1550
-
1551
- // Initialize the target playing state unless changed during the seek process
1552
- // In the future, we should allow for seeking in background. Currently, there's no
1553
- // way to know when the web application will call moveToForeground (i.e Before/After seek)
1554
- // Therefore, for now, we will assume the target is either paused or playing in ui unless
1555
- // specifically receiving a moveToBackground during the process.
1556
- // if (this._isPlaying && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
1557
- // this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
1558
- // }
1559
- this._targetSeekPlayingState = this._isPlaying ? TargetPlayingState.PLAYING_UI : TargetPlayingState.PAUSED;
1564
+ if (this._isPlaying) {
1565
+ if (!(lifecycle._inTransitionToForeground || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND) && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
1566
+ sdkLogger.info("seek in background", this._isPlaying);
1567
+ this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
1568
+ } else {
1569
+ this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
1570
+ }
1571
+ } else {
1572
+ this._targetSeekPlayingState = TargetPlayingState.PAUSED;
1573
+ }
1560
1574
 
1561
1575
  // The platform could be currently syncing audio/video using playback rate. Reset when performing seek.
1562
1576
  if (this._videoElement) {
@@ -1623,7 +1637,7 @@ class RemotePlayer extends RemotePlayerInterface {
1623
1637
 
1624
1638
  // If in TargetPlayingState.PAUSE, no need to resume.
1625
1639
  // Resume without awaiting to avoid blocking the seek process anymore
1626
- // In case where we aborted, we don't want to resume playback.
1640
+ // In case where we aborted (new load or unload called), we don't want to resume playback.
1627
1641
  if (!this._abortSeeking) {
1628
1642
  if (this._targetSeekPlayingState === TargetPlayingState.PLAYING_UI) {
1629
1643
  if (!this._isAudioSyncEnabled()) {
@@ -120,13 +120,27 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
120
120
  },
121
121
  "pause": () => {
122
122
  this._resetPlayPromise();
123
- this.remotePlayer.pause()
124
- .catch(error => {
125
- sdkLogger.error("Failed to pause remote player:", error);
126
- this.handleSenzaError(error.code, error.message || "Unknown pause error");
127
- });
128
- // Force move to foreground when pausing as current implementation of remote player doesn't support for exapmle to do load while in background
129
- lifecycle.moveToForeground();
123
+ if (!this._pauseForDelayedSeek) {
124
+ this.remotePlayer.pause()
125
+ .catch(error => {
126
+ sdkLogger.error("Failed to pause remote player:", error);
127
+ this.handleSenzaError(error.code, error.message || "Unknown pause error");
128
+ });
129
+ lifecycle.moveToForeground();
130
+ } else {
131
+ // in case of background seek, pause was called on local player, calling play() on local player will wait until "playing" event arrives (or timesout)
132
+ sdkLogger.info("senzaShakaPlayer pause Callback calling play() on local player. playing event should follow");
133
+ this._pauseForDelayedSeek = false;
134
+ this.videoElement.play();
135
+ }
136
+ },
137
+ "seeked" : () => {
138
+ // In case of background seek - we need to pause the local player after "seeked" until remote player is "playing" to avoid difference between the players.
139
+ if (this.isInRemotePlayback) {
140
+ this._pauseForDelayedSeek = true;
141
+ sdkLogger.info("senzaShakaPlayer seeked Callback: pausing local player. play should follow");
142
+ this.videoElement.pause();
143
+ }
130
144
  }
131
145
  };
132
146
 
@@ -348,7 +362,7 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
348
362
  this._minSuggestedPresentationDelay = uiSettings.minSuggestedPresentationDelay;
349
363
  sdkLogger.info(`Using configured minSuggestedPresentationDelay: ${this._minSuggestedPresentationDelay}s`);
350
364
  }
351
-
365
+ this._pauseForDelayedSeek = false;
352
366
  // if video element is provided, add the listeres here. In this case ,there is no need to call attach.
353
367
  if (videoElement) {
354
368
  this._attach(videoElement);
@@ -135,6 +135,24 @@ export function isSubtitlesTranslationPattern(lang) {
135
135
  return (lang?.toString() || "").startsWith("*:");
136
136
  }
137
137
 
138
+ export function isAppVersionAboveOrEqual(appVersion, minAppVersion) {
139
+ const version = appVersion.replace(/-dev|-prod/g, "").replace(/^.*\//, "");
140
+
141
+ sdkLogger.info(`isAppVersionAboveOrEqual: Comparing appVersion: ${version} with minAppVersion: ${minAppVersion}`);
142
+ const v1Parts = version.split(".").map(num => parseInt(num, 10) || 0);
143
+ const v2Parts = minAppVersion.split(".").map(num => parseInt(num, 10) || 0);
144
+
145
+ const maxLength = Math.max(v1Parts.length, v2Parts.length);
146
+ for (let i = 0; i < maxLength; i++) {
147
+ const v1 = v1Parts[i] ?? 0; // default missing parts to 0
148
+ const v2 = v2Parts[i] ?? 0;
149
+ if (v1 > v2) return true; // `appVersion` is newer
150
+ if (v1 < v2) return false; // `appVersion` is older
151
+ }
152
+ return true; // equal.
153
+ }
154
+
155
+
138
156
  // These StreamType constants are used as a mask for the client to play/stop the specific types
139
157
  export const StreamType = Object.freeze({
140
158
  NONE: 0,
@@ -1,4 +1,5 @@
1
1
  import * as shaka from "shaka-player";
2
+ import shakaUI from "shaka-player/dist/shaka-player.ui.js";
2
3
 
3
4
  // Define custom error category
4
5
  shaka.util.Error.Category.SENZA_PLAYER_ERROR = 50;
@@ -6,7 +7,7 @@ shaka.util.Error.Code.SENZA_PLAYER_ERROR = 10500;
6
7
 
7
8
  // Copy the shaka module and replace the Player class with SenzaShakaPlayer
8
9
  // if we don't Copy the shaka module, the Player class will be replaced for all the other modules that import shaka
9
- const senzaShaka = { ...shaka };
10
+ const senzaShaka = { ...shaka, ui: shakaUI.ui };
10
11
 
11
12
 
12
13
  /**
@@ -1 +1 @@
1
- export const version = "4.4.0";
1
+ export const version = "4.4.1-4ca75a1.0";