senza-sdk 4.4.1-daf0312.0 → 4.4.2-efcb53d.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.
@@ -12,9 +12,27 @@
12
12
 
13
13
  /*
14
14
  @license
15
- EME Encryption Scheme Polyfill
16
- Copyright 2019 Google LLC
17
- SPDX-License-Identifier: Apache-2.0
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.
18
36
  */
19
37
 
20
38
  /*
@@ -45,6 +63,13 @@
45
63
  SPDX-License-Identifier: Apache-2.0
46
64
  */
47
65
 
66
+ /*
67
+ @license
68
+ Shaka Player
69
+ Copyright 2025 Google LLC
70
+ SPDX-License-Identifier: Apache-2.0
71
+ */
72
+
48
73
  /*
49
74
  @license
50
75
  tXml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senza-sdk",
3
- "version": "4.4.1-daf0312.0",
3
+ "version": "4.4.2-efcb53d.0",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
@@ -53,15 +53,15 @@
53
53
  "statements": 93.46
54
54
  },
55
55
  "src/interface": {
56
- "branches": 68.42,
57
- "functions": 39.63,
58
- "lines": 64.94,
59
- "statements": 65.32
56
+ "branches": 63.9,
57
+ "functions": 39.28,
58
+ "lines": 64.61,
59
+ "statements": 65
60
60
  }
61
61
  }
62
62
  },
63
63
  "dependencies": {
64
- "shaka-player": "^4.12.5",
64
+ "shaka-player": "4.15.8",
65
65
  "moment": "^2.30.1"
66
66
  }
67
67
  }
package/src/api.js CHANGED
@@ -308,6 +308,9 @@ let ShakaPlayerImplementation;
308
308
  let shakaImplementation;
309
309
 
310
310
  if (!isRunningE2E()) {
311
+ // ------------------------------------------------------------------------------------------------
312
+ // -- Do NOT change this log, as ui-streamer is checking this string to detect abnormal behavior --
313
+ // ------------------------------------------------------------------------------------------------
311
314
  sdkLogger.warn("Senza SDK is not running in E2E environment. Some features may not work as expected.");
312
315
 
313
316
  lifecycle = new Lifecycle();
@@ -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,10 +312,10 @@ 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
- const request = { target: "TC", waitForResponse: false, internalAction: "uiExit", message: JSON.stringify({ type: "uiStandbyRequest", autoPlay: remotePlayer._autoPlay, fcid: FCID }) };
318
+ const request = { target: "TC", waitForResponse: false, internalAction: "uiExit", message: JSON.stringify({ type: "uiStandbyRequest", fcid: FCID }) };
317
319
  const logger = sdkLogger.withFields({ FCID });
318
320
  logger.log("lifecycle moveToUiStandby: sending uiStandbyRequest");
319
321
  window.cefQuery({
@@ -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 });
@@ -548,14 +559,15 @@ class Lifecycle extends LifecycleInterface {
548
559
  action: "play",
549
560
  fcid: FCID,
550
561
  audioLanguage,
551
- subtitlesLanguage
562
+ subtitlesLanguage,
563
+ autoTune: remotePlayer._autoTune
552
564
  };
553
565
  if (this._remotePlayerApiVersion >= 2) {
554
566
  message.type = "remotePlayer.play";
555
567
  message.class = "remotePlayer";
556
- message.switchMode = remotePlayer._isAudioSyncEnabled() ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS;
568
+ // in background, client expects to get NON_SEAMLESS switch mode.
569
+ message.switchMode = remotePlayer._isAudioSyncEnabled() && this._state !== this.UiState.BACKGROUND ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS;
557
570
  message.streamType = remotePlayer.textTrackVisibility ? (StreamType.AUDIO | StreamType.VIDEO | StreamType.SUBTITLE) : (StreamType.AUDIO | StreamType.VIDEO);
558
- message.autoPlay = remotePlayer._autoPlay;
559
571
  request = {
560
572
  target: "TC",
561
573
  waitForResponse: true,
@@ -568,7 +580,7 @@ class Lifecycle extends LifecycleInterface {
568
580
  }
569
581
  request = message;
570
582
  }
571
- logger.log(`lifecycle moveToBackground: sending play action audioLanguage=${message.audioLanguage} subtitlesLanguage=${message.subtitlesLanguage} textTrackVisibility=${remotePlayer.textTrackVisibility}`);
583
+ logger.log(`lifecycle moveToBackground: sending play action audioLanguage=${message.audioLanguage} subtitlesLanguage=${message.subtitlesLanguage} textTrackVisibility=${remotePlayer.textTrackVisibility} autoTune=${remotePlayer._autoTune}`);
572
584
  let timerId = 0;
573
585
  const timeBeforeSendingRequest = Date.now();
574
586
  const queryId = window.cefQuery({
@@ -577,14 +589,14 @@ class Lifecycle extends LifecycleInterface {
577
589
  onSuccess: () => {
578
590
  const duration = Date.now() - timeBeforeSendingRequest;
579
591
  logger.withFields({ duration }).log(`play completed successfully after ${duration} ms`);
580
- this._inTransition = false;
592
+ this._inTransitionToBackground = false;
581
593
  timerId = clearTimer(timerId);
582
594
  resolve();
583
595
  },
584
596
  onFailure: (code, msg) => {
585
597
  const duration = Date.now() - timeBeforeSendingRequest;
586
598
  logger.withFields({ duration }).log(`play failed after ${duration} ms. Error code: ${code}, error message: ${msg}`);
587
- this._inTransition = false;
599
+ this._inTransitionToBackground = false;
588
600
  timerId = clearTimer(timerId);
589
601
  reject(new SenzaError(code, msg));
590
602
  }
@@ -594,7 +606,7 @@ class Lifecycle extends LifecycleInterface {
594
606
  const timeout = this._remotePlayerConfirmationTimeout + 1000;
595
607
  timerId = setTimeout(() => {
596
608
  logger.log(`play reached timeout of ${timeout} ms, canceling query id ${queryId}`);
597
- this._inTransition = false;
609
+ this._inTransitionToBackground = false;
598
610
  window.cefQueryCancel(queryId);
599
611
  reject(new SenzaError(6000, `play reached timeout of ${timeout} ms`));
600
612
  }, timeout, queryId);
@@ -607,11 +619,11 @@ class Lifecycle extends LifecycleInterface {
607
619
 
608
620
  moveToBackground() {
609
621
  if (window.cefQuery) {
610
- if (this._inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
611
- sdkLogger.warn(`lifecycle moveToBackground: No need to transition to background, state: ${this._state} transition: ${this._inTransition}`);
622
+ const inTransition = this._isInTransition();
623
+ if (inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
624
+ sdkLogger.warn(`lifecycle moveToBackground: No need to transition to background, state: ${this._state} transition: ${inTransition}`);
612
625
  return Promise.resolve(false);
613
626
  }
614
-
615
627
  if (remotePlayer._isSeekingByApplication) {
616
628
  remotePlayer._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
617
629
  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: ""
@@ -180,7 +183,7 @@ class RemotePlayer extends RemotePlayerInterface {
180
183
  }
181
184
  }
182
185
  this._loadMode = playerState?.isLoaded ? this.LoadMode.LOADED : this.LoadMode.NOT_LOADED;
183
- this._autoPlay = false;
186
+ this._autoTune = false;
184
187
 
185
188
  this._isPlaying = window?.sessionStorage?.getItem("senzaSdk_isPlaying") === "true";
186
189
 
@@ -194,6 +197,9 @@ class RemotePlayer extends RemotePlayerInterface {
194
197
  this._remotePlayerConfirmationTimeout = uiStreamerSettings?.remotePlayerConfirmationTimeout ?? DEFAULT_REMOTE_PLAYER_CONFIRMATION_TIMEOUT;
195
198
  this._remotePlayerApiVersion = uiStreamerSettings?.remotePlayerApiVersion || 1;
196
199
  this._multiSeekDelay = uiStreamerSettings?.multiSeekDelay || MULTI_SEEK_DELAY_MSEC;
200
+ this._uiAvSyncIntervalMs = uiStreamerSettings?.uiAvSyncIntervalMs;
201
+ this._abrAvSyncIntervalMs = uiStreamerSettings?.abrAvSyncIntervalMs;
202
+ this._deviceAppVersion = sessionObj?.manifest?.["device-app"] || "";
197
203
 
198
204
  sdkLogger.info(`remotePLayer isPlaying=${this._isPlaying}`);
199
205
 
@@ -205,6 +211,7 @@ class RemotePlayer extends RemotePlayerInterface {
205
211
  }
206
212
  this._availabilityStartTime = playbackMetadata.availabilityStartTime;
207
213
  this._updateTracks(playbackMetadata);
214
+
208
215
  }
209
216
 
210
217
  /**
@@ -528,7 +535,8 @@ class RemotePlayer extends RemotePlayerInterface {
528
535
  };
529
536
  let waitForResponse = false;
530
537
  if (this._remotePlayerApiVersion >= 2) {
531
- message.switchMode = this._isAudioSyncEnabled() ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS;
538
+ // in background, client expects to get NON_SEAMLESS switch mode.
539
+ message.switchMode = this._isAudioSyncEnabled() && lifecycle.state !== lifecycle.UiState.BACKGROUND ? SwitchMode.SEAMLESS : SwitchMode.NON_SEAMLESS;
532
540
  message.streamType = streamType;
533
541
  waitForResponse = true;
534
542
 
@@ -781,6 +789,12 @@ class RemotePlayer extends RemotePlayerInterface {
781
789
  playbackPosition,
782
790
  fcid: FCID
783
791
  };
792
+ if (this._uiAvSyncIntervalMs !== undefined) {
793
+ message.uiAvSyncIntervalMs = this._uiAvSyncIntervalMs;
794
+ }
795
+ if (this._abrAvSyncIntervalMs !== undefined) {
796
+ message.abrAvSyncIntervalMs = this._abrAvSyncIntervalMs;
797
+ }
784
798
  if (this._remotePlayerApiVersion >= 2) {
785
799
  message.type = "remotePlayer.load";
786
800
  message.class = "remotePlayer";
@@ -920,10 +934,11 @@ class RemotePlayer extends RemotePlayerInterface {
920
934
  * remotePlayer.play();
921
935
  * lifecycle.moveToBackground();
922
936
  *
937
+ * @param {boolean} [autoTune=false] Should be true when the play was triggered automatically by the app and not by a user action.
923
938
  * @returns {Promise}
924
939
  * @throws {RemotePlayerError} error object contains code & msg
925
940
  */
926
- play(autoPlay = false) {
941
+ play(autoTune = false) {
927
942
  if (!this._isInitialized) {
928
943
  throw new RemotePlayerError(6500, "Cannot call play() if remote player is not initialized");
929
944
  }
@@ -934,7 +949,7 @@ class RemotePlayer extends RemotePlayerInterface {
934
949
  }
935
950
 
936
951
  this._changePlayMode(true);
937
- this._autoPlay = autoPlay;
952
+ this._autoTune = autoTune;
938
953
 
939
954
  if (this._isSetAudioInProgress) {
940
955
  sdkLogger.info("application requesting play during setAudioLanguage");
@@ -950,8 +965,13 @@ class RemotePlayer extends RemotePlayerInterface {
950
965
 
951
966
  // If seeking in progress, wait for seek to complete before playing
952
967
  if (this._isSeekingByApplication) {
953
- sdkLogger.info("application requesting play during seek");
954
- this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
968
+ if (this._inTransitionToForeground || lifecycle.state === lifecycle.UiState.FOREGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND) {
969
+ sdkLogger.info("application requesting play during seek. setting targetSeekPlayingState to PLAYING_UI");
970
+ this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
971
+ } else {
972
+ sdkLogger.info("application requesting play during seek. setting targetSeekPlayingState to PLAYING_ABR");
973
+ this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
974
+ }
955
975
  return Promise.resolve(true);
956
976
  }
957
977
  /*
@@ -1083,24 +1103,30 @@ class RemotePlayer extends RemotePlayerInterface {
1083
1103
  * @throws {RemotePlayerError} If the player is not initialized or not loaded.
1084
1104
  **/
1085
1105
  selectAudioTrack(audioTrackId) {
1106
+ return this._selectAudioTrack(audioTrackId, false);
1107
+ }
1108
+
1109
+ _selectAudioTrack(audioTrackId, force = false) {
1086
1110
  if (!this._isInitialized) {
1087
1111
  throw new RemotePlayerError(6500, "Cannot call selectAudioTrack() if remote player is not initialized");
1088
1112
  }
1089
1113
  if (this._loadMode !== this.LoadMode.LOADED) {
1090
1114
  throw new RemotePlayerError(6001, "Cannot call selectAudioTrack() if remote player is not loaded");
1091
1115
  }
1092
- let found = false;
1093
- const prevSelectedAudioTrack = this._selectedAudioTrack;
1094
- for (const track of this.getAudioTracks()) {
1095
- if (track.id === audioTrackId) {
1096
- found = true;
1097
- break;
1116
+ if (!force) {
1117
+ let found = false;
1118
+ for (const track of this.getAudioTracks()) {
1119
+ if (track.id === audioTrackId) {
1120
+ found = true;
1121
+ break;
1122
+ }
1123
+ }
1124
+ if (!found) {
1125
+ sdkLogger.warn(`Invalid audioTrackId ${audioTrackId}`);
1126
+ return Promise.resolve();
1098
1127
  }
1099
1128
  }
1100
- if (!found) {
1101
- sdkLogger.warn(`Invalid audioTrackId ${audioTrackId}`);
1102
- return Promise.resolve();
1103
- }
1129
+ const prevSelectedAudioTrack = this._selectedAudioTrack;
1104
1130
  if (this._selectedAudioTrack === audioTrackId) {
1105
1131
  return Promise.resolve(); // Audio language already selected
1106
1132
  }
@@ -1117,6 +1143,12 @@ class RemotePlayer extends RemotePlayerInterface {
1117
1143
  }
1118
1144
  }
1119
1145
 
1146
+ async selectAudioLanguage(language, role="", accessibilityPurposeCode=this.AccessibilityPurposeCode.NONE) {
1147
+ const trackId = this._generateSenzaTrackId(language, role, accessibilityPurposeCode);
1148
+ return this._selectAudioTrack(trackId, true);
1149
+ }
1150
+
1151
+
1120
1152
  /**
1121
1153
  * Handles the asynchronous selection of an audio track.
1122
1154
  * If the player is playing, it pauses before changing the track and resumes playback if necessary.
@@ -1210,6 +1242,11 @@ class RemotePlayer extends RemotePlayerInterface {
1210
1242
  return Promise.resolve(undefined);
1211
1243
  }
1212
1244
 
1245
+ async selectTextLanguage(language, role="", accessibilityPurposeCode=this.AccessibilityPurposeCode.NONE) {
1246
+ const trackId = this._generateSenzaTrackId(language, role, accessibilityPurposeCode);
1247
+ return this._selectTextTrack(trackId, true);
1248
+ }
1249
+
1213
1250
  /** Select a specific text (subtitle) track.
1214
1251
  * Track id should come from a call to getTextTracks.
1215
1252
  * If no tracks exist - this is a no-op.
@@ -1219,24 +1256,31 @@ class RemotePlayer extends RemotePlayerInterface {
1219
1256
  * @throws {RemotePlayerError} If the player is not initialized or not loaded.
1220
1257
  * */
1221
1258
  selectTextTrack(textTrackId) {
1259
+ return this._selectTextTrack(textTrackId, false);
1260
+ }
1261
+
1262
+ _selectTextTrack(textTrackId, force = false) {
1222
1263
  if (!this._isInitialized) {
1223
1264
  throw new RemotePlayerError(6500, "Cannot call selectTextTrack() if remote player is not initialized");
1224
1265
  }
1225
1266
  if (this._loadMode !== this.LoadMode.LOADED) {
1226
1267
  throw new RemotePlayerError(6001, "Cannot call selectTextTrack() if remote player is not loaded");
1227
1268
  }
1228
- let found = false;
1229
- const prevSelectedTextTrack = this._selectedSubtitlesTrack;
1230
- for (const track of this.getTextTracks()) {
1231
- if (track.id === textTrackId) {
1232
- found = true;
1233
- break;
1269
+ if (!force) {
1270
+ let found = false;
1271
+ for (const track of this.getTextTracks()) {
1272
+ if (track.id === textTrackId) {
1273
+ found = true;
1274
+ break;
1275
+ }
1276
+ }
1277
+ if (!found) {
1278
+ sdkLogger.warn(`Invalid textTrackId ${textTrackId}`);
1279
+ return Promise.resolve();
1234
1280
  }
1235
1281
  }
1236
- if (!found) {
1237
- sdkLogger.warn(`Invalid textTrackId ${textTrackId}`);
1238
- return Promise.resolve();
1239
- }
1282
+
1283
+ const prevSelectedTextTrack = this._selectedSubtitlesTrack;
1240
1284
  if (this._selectedSubtitlesTrack === textTrackId) {
1241
1285
  return Promise.resolve(); // Subtitle language already selected
1242
1286
  }
@@ -1512,6 +1556,8 @@ class RemotePlayer extends RemotePlayerInterface {
1512
1556
  * @private
1513
1557
  */
1514
1558
  async _startSeeking(playbackPosition) {
1559
+ const backgroundSeekMinClientAppVersion = "25.27.8";
1560
+
1515
1561
  if (this._isSetAudioInProgress || this._isSetSubtitlesInProgress) {
1516
1562
  sdkLogger.info("Seeking not supported while setAudioLanguage or setSubtitleLanguage are in progress.");
1517
1563
  return;
@@ -1527,10 +1573,12 @@ class RemotePlayer extends RemotePlayerInterface {
1527
1573
  return;
1528
1574
  }
1529
1575
  }
1530
-
1531
- // Only allow seeking in foreground. Still ignore the initialized local player seeking event above
1532
- if (this._remotePlayerApiVersion >= 2 && !this._isSeekingByPlatform && !this._isSeekingByApplication &&
1533
- (lifecycle.state === lifecycle.UiState.FOREGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND)) {
1576
+ const backgroundSeekSupported = isAppVersionAboveOrEqual(this._deviceAppVersion, backgroundSeekMinClientAppVersion);
1577
+ if (!backgroundSeekSupported && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
1578
+ sdkLogger.warn(`Seeking: background seek not supported for device app version: ${this._deviceAppVersion}`);
1579
+ }
1580
+ if (this._remotePlayerApiVersion >= 2 && !this._isSeekingByPlatform && !this._isSeekingByApplication
1581
+ && (lifecycle.state === lifecycle.UiState.FOREGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND || backgroundSeekSupported)) {
1534
1582
  this._atomicSeek();
1535
1583
  } else {
1536
1584
  sdkLogger.info(`Seeking: skipping seeking event to currentTime: ${playbackPosition}, internalSeek: ${this._isSeekingByPlatform}, localPlayerSeek: ${this._isSeekingByApplication}, state: ${lifecycle.state}`);
@@ -1549,16 +1597,16 @@ class RemotePlayer extends RemotePlayerInterface {
1549
1597
  * */
1550
1598
  async _atomicSeek() {
1551
1599
  sdkLogger.info("Seeking: local video element seeking start while isPlaying: ", this._isPlaying);
1552
-
1553
- // Initialize the target playing state unless changed during the seek process
1554
- // In the future, we should allow for seeking in background. Currently, there's no
1555
- // way to know when the web application will call moveToForeground (i.e Before/After seek)
1556
- // Therefore, for now, we will assume the target is either paused or playing in ui unless
1557
- // specifically receiving a moveToBackground during the process.
1558
- // if (this._isPlaying && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
1559
- // this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
1560
- // }
1561
- this._targetSeekPlayingState = this._isPlaying ? TargetPlayingState.PLAYING_UI : TargetPlayingState.PAUSED;
1600
+ if (this._isPlaying) {
1601
+ if (!(lifecycle._inTransitionToForeground || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND) && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
1602
+ sdkLogger.info("seek in background", this._isPlaying);
1603
+ this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
1604
+ } else {
1605
+ this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
1606
+ }
1607
+ } else {
1608
+ this._targetSeekPlayingState = TargetPlayingState.PAUSED;
1609
+ }
1562
1610
 
1563
1611
  // The platform could be currently syncing audio/video using playback rate. Reset when performing seek.
1564
1612
  if (this._videoElement) {
@@ -1625,14 +1673,13 @@ class RemotePlayer extends RemotePlayerInterface {
1625
1673
 
1626
1674
  // If in TargetPlayingState.PAUSE, no need to resume.
1627
1675
  // Resume without awaiting to avoid blocking the seek process anymore
1628
- // In case where we aborted, we don't want to resume playback.
1676
+ // In case where we aborted (new load or unload called), we don't want to resume playback.
1629
1677
  if (!this._abortSeeking) {
1630
1678
  if (this._targetSeekPlayingState === TargetPlayingState.PLAYING_UI) {
1631
- if (!this._isAudioSyncEnabled()) {
1632
- return Promise.resolve(true);
1679
+ // When in foreground back from ABR, we are considered playing. But, if AIU is not enabled, we shouldn't resume it.
1680
+ if (this._isAudioSyncEnabled()) {
1681
+ this._play(StreamType.AUDIO);
1633
1682
  }
1634
- // resume audio play only if _isAudioSyncEnabled
1635
- this._play(StreamType.AUDIO);
1636
1683
  } else if (this._targetSeekPlayingState === TargetPlayingState.PLAYING_ABR) {
1637
1684
  lifecycle._moveToBackground();
1638
1685
  }
@@ -1776,6 +1823,12 @@ class RemotePlayer extends RemotePlayerInterface {
1776
1823
  return setLanguageError ? Promise.reject(setLanguageError) : Promise.resolve();
1777
1824
  }
1778
1825
 
1826
+ _generateSenzaTrackId(language, role, accessibilityPurpose) {
1827
+ const l = language || "";
1828
+ const r = role || "";
1829
+ const ap = accessibilityPurpose || "";
1830
+ return `${l}:${r}:${ap}`;
1831
+ }
1779
1832
  _setScreenBlackout(blackoutTime) {
1780
1833
  if (window.cefQuery) {
1781
1834
  const FCID = getFCID();