senza-sdk 4.5.8 → 4.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senza-sdk",
3
- "version": "4.5.8",
3
+ "version": "4.6.0",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
@@ -18,6 +18,8 @@
18
18
  "build:interface": "npx webpack --config interface.webpack.config.js",
19
19
  "prebuild:implementation": "npm run version:output",
20
20
  "build:implementation": "npx webpack --config implementation.webpack.config.js",
21
+ "build:dual-decode-sample": "npm run build && mkdir -p samples/dualDecode/sdk && cp dist/bundle.js dist/implementation.bundle.js samples/dualDecode/sdk/ && npm run build:dual-decode-sample:app",
22
+ "build:dual-decode-sample:app": "npx webpack --config samples/dualDecode/webpack.config.js --context samples/dualDecode",
21
23
  "test": "jest --maxWorkers=20% --coverage --verbose",
22
24
  "testall": "npm run test && npm run eslint --fix",
23
25
  "version:output": "echo 'export const version = \"'$npm_package_version'\";' > src/interface/version.js"
@@ -60,7 +60,7 @@ export async function init(interfaceApiVersion, showSequenceFunc, initSequenceFu
60
60
 
61
61
  if (window.cefQuery) {
62
62
 
63
- // First, check compatability with ui-streamer api version
63
+ // First, check compatibility with ui-streamer api version
64
64
  await new Promise((resolve, reject) => {
65
65
  window.cefQuery({
66
66
  request: "apiVersion " + API_VERSION,
@@ -140,13 +140,15 @@ export async function init(interfaceApiVersion, showSequenceFunc, initSequenceFu
140
140
  }
141
141
  }
142
142
 
143
- // Initialize lifecycle first to make sure the state is updated.
144
- await lifecycle._init(sessionInfoObj?.settings?.["ui-streamer"], triggerEvent);
145
- await remotePlayer._init(sessionInfoObj, triggerEvent);
143
+ await Promise.all([
144
+ lifecycle._init(sessionInfoObj?.settings?.["ui-streamer"], triggerEvent),
145
+ remotePlayer._init(sessionInfoObj, triggerEvent),
146
+ displayManager._init()
147
+ ]);
148
+
146
149
  alarmManager._init();
147
150
  messageManager._init();
148
151
  backgroundRenderControl._init();
149
- await displayManager._init();
150
152
  sdkLogger.log("All submodules initialized");
151
153
 
152
154
 
@@ -4,6 +4,7 @@ import { parseEdid } from "./edidParser";
4
4
  import { EventListenersManager } from "./eventListenersManager";
5
5
  import { bus, Events } from "./eventBus";
6
6
  import { lifecycle } from "./lifecycle";
7
+ import { sessionInfo } from "./SessionInfo";
7
8
 
8
9
 
9
10
  export class DisplayManager extends DisplayManagerInterface {
@@ -38,6 +39,12 @@ export class DisplayManager extends DisplayManagerInterface {
38
39
  }
39
40
 
40
41
  async _init() {
42
+ const enableCec = sessionInfo?.sessionInfoObj?.settings?.client?.hdmi?.cec?.enable_cec;
43
+ if (enableCec === false) {
44
+ sdkLogger.log("CEC disabled via config (client.hdmi.cec.enable_cec=false), skipping DisplayManager init");
45
+ this._setDisplayProperties({ connection: this.DisplayConnectionStatus.UNKNOWN });
46
+ return;
47
+ }
41
48
  sdkLogger.log("Initializing DisplayManager");
42
49
  await this._updateDisplayInfo();
43
50
  if (!this._isInitialized) {
@@ -27,6 +27,12 @@ const DEFAULT_AUTO_SUSPEND_IDLE_DELAY = 60;
27
27
  // Session storage key for tracking timer-triggered background state
28
28
  const BACKGROUND_TIMER_KEY = "senzaSDK_backgroundTriggeredByTimer";
29
29
 
30
+ /** @enum {string} */
31
+ const UiTransition = Object.freeze({
32
+ START_UI: "startUi",
33
+ STOP_UI: "stopUi"
34
+ });
35
+
30
36
  class Lifecycle extends LifecycleInterface {
31
37
  constructor() {
32
38
  super();
@@ -552,6 +558,58 @@ class Lifecycle extends LifecycleInterface {
552
558
  return Promise.resolve(true);
553
559
  }
554
560
 
561
+ /**
562
+ * @private Sends a simple message to the client and resolves/rejects based on the response.
563
+ * Used by the dual decode startUi/stopUi transitions.
564
+ * @param {UiTransition} type the message type to send
565
+ * @returns {Promise<boolean>}
566
+ */
567
+ _sendUiTransition(type) {
568
+ if (window.cefQuery) {
569
+ return new Promise((resolve, reject) => {
570
+ const FCID = getFCID();
571
+ const logger = sdkLogger.withFields({ FCID });
572
+ const message = { type, fcid: FCID };
573
+ // Mirror V2 remotePlayer lifecycle: ui-streamer resumes/pauses locally via internalAction
574
+ // while TC receives startUi/stopUi on the datachannel.
575
+ const internalAction = type === UiTransition.START_UI ? "uiActive" : "uiExit";
576
+ // TC confirms stopUi with isResponse; startUi is fire-and-forget until TC adds a response.
577
+ const waitForResponse = type !== UiTransition.START_UI;
578
+ const request = { target: "TC", waitForResponse, internalAction, message: JSON.stringify(message) };
579
+ window.cefQuery({
580
+ request: JSON.stringify(request),
581
+ persistent: false,
582
+ onSuccess: () => {
583
+ logger.log(`${type} successfully sent`);
584
+ resolve(true);
585
+ },
586
+ onFailure: (code, msg) => {
587
+ logger.error(`${type} failed: ${code} ${msg}`);
588
+ reject(new SenzaError(code, msg));
589
+ }
590
+ });
591
+ });
592
+ }
593
+ sdkLogger.warn(`lifecycle ${type}: window.cefQuery is undefined`);
594
+ return Promise.resolve(true);
595
+ }
596
+
597
+ /**
598
+ * @private Starts the UI streaming.
599
+ * @returns {Promise<boolean>}
600
+ */
601
+ _startUi() {
602
+ return this._sendUiTransition(UiTransition.START_UI);
603
+ }
604
+
605
+ /**
606
+ * @private Stops the UI streaming.
607
+ * @returns {Promise<boolean>}
608
+ */
609
+ _stopUi() {
610
+ return this._sendUiTransition(UiTransition.STOP_UI);
611
+ }
612
+
555
613
  get state() {
556
614
  if (!this._isInitialized) {
557
615
  this._state = this.UiState.UNKNOWN;
@@ -725,6 +783,11 @@ class Lifecycle extends LifecycleInterface {
725
783
  this._isBackgroundTriggeredByTimer = false;
726
784
 
727
785
  if (window.cefQuery) {
786
+ if (remotePlayer._isDualDecodeActive()) {
787
+ // Cancel a pending dual-decode background transition that is waiting for the playing event.
788
+ this._inTransitionToBackground = false;
789
+ }
790
+
728
791
  const inTransition = this._isInTransition();
729
792
  if (inTransition || this._isForegroundOrTransitioning()) {
730
793
  sdkLogger.warn(`lifecycle moveToForeground: No need to transition to foreground, state: ${this._state} transition: ${inTransition}`);
@@ -746,11 +809,23 @@ class Lifecycle extends LifecycleInterface {
746
809
  this._inTransitionToForeground = true;
747
810
  bus.dispatchEvent(new Event(Events.LifecycleForeground));
748
811
  const FCID = getFCID();
812
+
749
813
  if (this._remotePlayerApiVersion >= 2) {
750
814
  // 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.
751
815
  if (remotePlayer._isSeekingByApplication && remotePlayer._targetSeekPlayingState === TargetPlayingState.PLAYING_ABR) {
752
816
  remotePlayer._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
753
817
  }
818
+
819
+ if (remotePlayer._isDualDecodeActive()) {
820
+ // In dual decode mode, returning to the UI only requires (re)showing the UI on top of the ABR video.
821
+ // We must not stop the remote player playback, so skip the v2 uiActive/stop and legacy uiActiveRequest paths.
822
+ try {
823
+ return await this._startUi();
824
+ } finally {
825
+ this._inTransitionToForeground = false;
826
+ }
827
+ }
828
+
754
829
  return new Promise((resolve, reject) => {
755
830
  const FCID = getFCID();
756
831
  const logger = sdkLogger.withFields({ FCID });
@@ -817,6 +892,31 @@ class Lifecycle extends LifecycleInterface {
817
892
  return Promise.resolve(false);
818
893
  }
819
894
 
895
+ /**
896
+ * @private Handles the dual decode background transition: hide the UI via stopUi.
897
+ * Does not send any play/uiExit message. If the remote "playing" event has not arrived yet,
898
+ * stopUi is still called immediately and a warning is logged.
899
+ *
900
+ * @returns {Promise<boolean>} stopUi result
901
+ */
902
+ _moveToBackgroundDualDecode() {
903
+ if (remotePlayer._isPlayingEventPending) {
904
+ sdkLogger.warn("dual decode moveToBackground: moving to background before the playing event arrived; stopping the UI now");
905
+ } else {
906
+ sdkLogger.info("dual decode moveToBackground: playing event already arrived, stopping the UI");
907
+ }
908
+ this._inTransitionToBackground = true;
909
+ return this._stopUi()
910
+ .then((result) => {
911
+ this._inTransitionToBackground = false;
912
+ return result;
913
+ })
914
+ .catch((error) => {
915
+ this._inTransitionToBackground = false;
916
+ throw error;
917
+ });
918
+ }
919
+
820
920
  _moveToBackground(switchMode = SwitchMode.SEAMLESS) {
821
921
  /*const inTransition = this._isInTransition();
822
922
  if (inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
@@ -846,6 +946,11 @@ class Lifecycle extends LifecycleInterface {
846
946
  return this._moveToUiStandby();
847
947
  }
848
948
  remotePlayer._changePlayMode(true);
949
+
950
+ if (remotePlayer._isDualDecodeActive()) {
951
+ return this._moveToBackgroundDualDecode();
952
+ }
953
+
849
954
  this._inTransitionToBackground = true;
850
955
  return new Promise((resolve, reject) => {
851
956
  const FCID = getFCID();
@@ -4,10 +4,13 @@ import {
4
4
  getFCID,
5
5
  generatePlaybackId,
6
6
  isAudioSyncConfigured,
7
+ isDualDecodeEnabled,
8
+ getDualDecodeAbrSurfaceSize,
7
9
  clearTimer,
8
10
  sdkLogger,
9
11
  StreamType,
10
12
  SwitchMode,
13
+ VideoLayer,
11
14
  delay,
12
15
  SeekState,
13
16
  TargetPlayingState,
@@ -26,6 +29,7 @@ const MULTI_SEEK_DELAY_MSEC = 250;
26
29
  const LOAD_START_SYNC_DIFF = 1;
27
30
  const INVALID_SET_PLAYABLE_URI_CODE = 99;
28
31
  export const ONE_DAY_SECONDS = 86400;
32
+ export const VOD_INIT_SEEK_EPSILON_SEC = 1;
29
33
  const BACKGROUND_LOAD_MIN_SDK_VERSION = "4.5.0";
30
34
 
31
35
  // Default timeout for waiting for the remote player confirmation
@@ -68,6 +72,8 @@ function setPlaybackInfo(playbackInfo) {
68
72
  }
69
73
 
70
74
  class RemotePlayer extends RemotePlayerInterface {
75
+ VideoLayer = VideoLayer;
76
+
71
77
  constructor() {
72
78
  super();
73
79
  /**
@@ -140,6 +146,14 @@ class RemotePlayer extends RemotePlayerInterface {
140
146
  */
141
147
  this._isSeekingByPlatform = false;
142
148
 
149
+ /**
150
+ * Start position (seconds) from the most recent load(). Used to ignore the local player's
151
+ * first seeking event when VOD is loaded from a non-zero position (Shaka init seek).
152
+ * @type {number}
153
+ * @private
154
+ */
155
+ this._loadStartPosition = 0;
156
+
143
157
  /**
144
158
  * @type {boolean}
145
159
  * @description field indicating if the remote player is playing abr or not. Should be changed when starting/stoping playback
@@ -154,6 +168,13 @@ class RemotePlayer extends RemotePlayerInterface {
154
168
  */
155
169
  this._playbackId = "";
156
170
 
171
+ /**
172
+ * @type {boolean}
173
+ * @description True while a dual-decode play() is awaiting the remote "playing" event.
174
+ * @private
175
+ */
176
+ this._isPlayingEventPending = false;
177
+
157
178
  /**
158
179
  * Anchor position in seconds used by getPlaybackPosition().
159
180
  * @type {number}
@@ -213,14 +234,18 @@ class RemotePlayer extends RemotePlayerInterface {
213
234
  this._autoTune = false;
214
235
 
215
236
  this._isPlaying = window?.sessionStorage?.getItem("senzaSdk_isPlaying") === "true";
237
+ this._textTrackVisibility = window?.sessionStorage?.getItem("senzaSdk_textTrackVisibility") === "true";
216
238
  if (this._isPlaying) {
217
239
  this._playbackPositionAnchor = getPlaybackInfo()?.playbackPosition || 0;
218
240
  this._startPlaybackClock();
241
+
242
+ // UI release while playing: local player may load and seek; seed load start so that seek is treated as init.
243
+ this._loadStartPosition = this.currentTime;
219
244
  } else {
220
245
  this._stopPlaybackClock();
221
246
  }
222
247
 
223
- sdkLogger.info(`RemotePlayer initialized with load mode: ${this._loadMode}, senzaSdk_isPlaying: ${this._isPlaying}`);
248
+ sdkLogger.info(`RemotePlayer initialized with load mode: ${this._loadMode}, senzaSdk_isPlaying: ${this._isPlaying}, senzaSdk_textTrackVisibility: ${this._textTrackVisibility}`);
224
249
  this._loadedUrl = playerState?.playbackUrl || "";
225
250
  // Make sure that the event listeners are registered only once
226
251
  if (!this._isInitialized) {
@@ -270,6 +295,9 @@ class RemotePlayer extends RemotePlayerInterface {
270
295
  typeof document !== "undefined" && document.addEventListener("hs/remotePlayerEvent", (e) => {
271
296
  if (this._hasPlaybackIdMismatch(e?.detail, "hs/remotePlayerEvent")) return;
272
297
  sdkLogger.info("Got hs/remotePlayerEvent event with detail", JSON.stringify(e?.detail));
298
+ if (e?.detail?.eventName === "playing") {
299
+ this._isPlayingEventPending = false;
300
+ }
273
301
  this.dispatchEvent(new Event(e?.detail?.eventName));
274
302
  });
275
303
 
@@ -585,6 +613,8 @@ class RemotePlayer extends RemotePlayerInterface {
585
613
  this._availabilityStartTime = undefined;
586
614
  this._isSeekingByPlatform = false;
587
615
  this._isSeekingByApplication = false;
616
+ this._isPlayingEventPending = false;
617
+ this._loadStartPosition = 0;
588
618
  this._stopPlaybackClock();
589
619
  this._playbackPositionAnchor = 0;
590
620
  }
@@ -685,6 +715,12 @@ class RemotePlayer extends RemotePlayerInterface {
685
715
  } else if (!this.textTrackVisibility) {
686
716
  message.subtitlesLanguage = "";
687
717
  }
718
+ if (switchMode === SwitchMode.DUAL_DECODE) {
719
+ message.switchMode = SwitchMode.DUAL_DECODE;
720
+ message.streamType = streamType;
721
+ waitForResponse = true;
722
+ this._isPlayingEventPending = true;
723
+ }
688
724
  logger.log(`remotePlayer play: sending play action remotePlayer._isPlaying: ${this._isPlaying} audioLanguage=${message.audioLanguage} subtitlesLanguage=${message.subtitlesLanguage} textTrackVisibility=${this.textTrackVisibility}`);
689
725
  const request = { target: "TC", waitForResponse: waitForResponse, message: JSON.stringify(message) };
690
726
  return new Promise((resolve, reject) => {
@@ -703,6 +739,7 @@ class RemotePlayer extends RemotePlayerInterface {
703
739
  const duration = Date.now() - timeBeforeSendingRequest;
704
740
  logger.withFields({ duration }).log(`play failed after ${duration} ms. Error code: ${code}, error message: ${msg}`);
705
741
  timerId = clearTimer(timerId);
742
+ this._isPlayingEventPending = false;
706
743
  reject(new RemotePlayerError(code, msg));
707
744
  }
708
745
  });
@@ -727,7 +764,7 @@ class RemotePlayer extends RemotePlayerInterface {
727
764
 
728
765
  if (window.cefQuery) {
729
766
  const isForegroundState = lifecycle.state === lifecycle.UiState.FOREGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND;
730
- if (this._remotePlayerApiVersion >= 2 && !this._isAudioSyncEnabled() && isForegroundState) {
767
+ if (this._remotePlayerApiVersion >= 2 && !this._isAudioSyncEnabled() && !this._isDualDecodeActive() && isForegroundState) {
731
768
  sdkLogger.info("remotePlayer pause: application in foreground, remote player is not playing.");
732
769
  return Promise.resolve();
733
770
  }
@@ -793,6 +830,12 @@ class RemotePlayer extends RemotePlayerInterface {
793
830
  fcid: FCID,
794
831
  playbackId: this._playbackId
795
832
  };
833
+ if (this._isDualDecodeActive()) {
834
+ message.switchMode = SwitchMode.DUAL_DECODE;
835
+ if (streamType & StreamType.VIDEO) {
836
+ this._isPlayingEventPending = false;
837
+ }
838
+ }
796
839
  const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
797
840
  return new Promise((resolve, reject) => {
798
841
  let timerId = 0;
@@ -920,6 +963,7 @@ class RemotePlayer extends RemotePlayerInterface {
920
963
  this._changePlayMode(false);
921
964
  return new Promise((resolve, reject) => {
922
965
  const playbackPosition = position || 0;
966
+ this._loadStartPosition = playbackPosition;
923
967
  if (reset) {
924
968
  // first, zero playback position
925
969
  setPlaybackInfo({ playbackPosition: 0 });
@@ -1026,6 +1070,7 @@ class RemotePlayer extends RemotePlayerInterface {
1026
1070
  this._abortSetAudioLanguage = true;
1027
1071
  this._abortSetSubtitleLanguage = true;
1028
1072
  this._abortSeeking = true;
1073
+ this._isPlayingEventPending = false;
1029
1074
  const previousLoadMode = this._loadMode;
1030
1075
  this._changeLoadMode(this.LoadMode.UNLOADING);
1031
1076
  this._changePlayMode(false);
@@ -1113,6 +1158,17 @@ class RemotePlayer extends RemotePlayerInterface {
1113
1158
  this._changePlayMode(true);
1114
1159
  this._autoTune = autoTune;
1115
1160
 
1161
+ if (this._isDualDecodeActive()) {
1162
+ // In dual decode mode, ABR video is rendered over the UI video, so we always play audio + video
1163
+ // (and subtitles when visible) using the DUAL_DECODE switch mode, bypassing the foreground/audio-sync
1164
+ // early returns below. Start the waitForPlaying timer so callers (e.g. moveToBackground)
1165
+ // can wait for the remote player to actually start playing.
1166
+ const dualDecodeStreamType = this.textTrackVisibility
1167
+ ? (StreamType.AUDIO | StreamType.VIDEO | StreamType.SUBTITLE)
1168
+ : (StreamType.AUDIO | StreamType.VIDEO);
1169
+ return this._play(dualDecodeStreamType, SwitchMode.DUAL_DECODE);
1170
+ }
1171
+
1116
1172
  if (this._isSetAudioInProgress) {
1117
1173
  sdkLogger.info("application requesting play during setAudioLanguage");
1118
1174
  this._targetSetAudioPlayingState = TargetPlayingState.PLAYING_UI;
@@ -1198,6 +1254,18 @@ class RemotePlayer extends RemotePlayerInterface {
1198
1254
  return this._pause();
1199
1255
  }
1200
1256
 
1257
+ /** Stop playback of all streams (audio, video and subtitles).
1258
+ * @returns {Promise}
1259
+ * @throws {RemotePlayerError} error object contains code & msg
1260
+ *
1261
+ * */
1262
+ stop() {
1263
+ if (!this._isInitialized) {
1264
+ throw new RemotePlayerError(6500, "Cannot call stop() if remote player is not initialized");
1265
+ }
1266
+ return this._stop(StreamType.AUDIO | StreamType.VIDEO | StreamType.SUBTITLE);
1267
+ }
1268
+
1201
1269
  /** Get the currently loaded uri, or empty string is player is not loaded.
1202
1270
  * @returns {string} loaded uri
1203
1271
  * @throws {RemotePlayerError} error object contains code & msg
@@ -1588,14 +1656,33 @@ class RemotePlayer extends RemotePlayerInterface {
1588
1656
  // Convert to boolean in case apps pass 0/1 instead false/true.
1589
1657
  const newVisibility = !!visible;
1590
1658
 
1659
+ this._textTrackVisibility = newVisibility;
1660
+ window?.sessionStorage?.setItem("senzaSdk_textTrackVisibility", String(newVisibility));
1661
+
1591
1662
  if (oldVisibility === newVisibility) {
1592
1663
  return;
1593
1664
  }
1594
1665
 
1595
- this._textTrackVisibility = newVisibility;
1596
- if (!this._textTrackVisibility) {
1597
- // Setting the visibility to false clears any previous selections user has done
1598
- this._selectedSubtitlesTrack = "";
1666
+ if (this._remotePlayerApiVersion <= 2) {
1667
+ if (!this._textTrackVisibility) {
1668
+ // For older versions, we need to clear the selected subtitles track
1669
+ this._selectedSubtitlesTrack = "";
1670
+ }
1671
+ return;
1672
+ }
1673
+
1674
+ if (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND) {
1675
+ if (!this._textTrackVisibility) {
1676
+ this._stop(StreamType.SUBTITLE).catch((error) => {
1677
+ sdkLogger.warn(`remotePlayer setTextTrackVisibility: stop subtitles failed with error ${error.message}.`);
1678
+ });
1679
+ } else {
1680
+ this._play(StreamType.SUBTITLE).catch((error) => {
1681
+ sdkLogger.warn(`remotePlayer setTextTrackVisibility: play subtitles failed with error ${error.message}.`);
1682
+ });
1683
+ }
1684
+ } else {
1685
+ sdkLogger.info("remotePlayer setTextTrackVisibility V3+: not updating remote subtitle stream outside background");
1599
1686
  }
1600
1687
  }
1601
1688
 
@@ -1654,6 +1741,109 @@ class RemotePlayer extends RemotePlayerInterface {
1654
1741
  return this._setScreenBlackout(0);
1655
1742
  }
1656
1743
 
1744
+ /**
1745
+ * @private
1746
+ * Whether dual decode is active for this player (remote player API v2+ and session capability/settings enabled).
1747
+ * @returns {boolean}
1748
+ */
1749
+ _isDualDecodeActive() {
1750
+ return this._remotePlayerApiVersion >= 2 && isDualDecodeEnabled();
1751
+ }
1752
+
1753
+ /** Set the position and size of the remote player (ABR) video on screen, or make it fullscreen.
1754
+ * Only available when dual decode is enabled.
1755
+ * The given rectangle is expressed in the UI coordinate space (window.innerWidth/window.innerHeight)
1756
+ * and is validated and scaled to the client's ABR surface size before being sent to the client.
1757
+ * @param {Object} rect
1758
+ * @param {number} [rect.x] horizontal position in px (UI coordinate space)
1759
+ * @param {number} [rect.y] vertical position in px (UI coordinate space)
1760
+ * @param {number} [rect.width] width in px (UI coordinate space)
1761
+ * @param {number} [rect.height] height in px (UI coordinate space)
1762
+ * @param {boolean} [rect.fullScreen=false] when true, the video is rendered fullscreen and the rect is ignored
1763
+ * @param {VideoLayer} [rect.layer=VideoLayer.FRONT] which layer the ABR video should be rendered on
1764
+ * @returns {Promise}
1765
+ * @throws {RemotePlayerError} error object contains code & msg
1766
+ */
1767
+ setVideoRect({ x, y, width, height, fullScreen = false, layer } = {}) {
1768
+ if (!this._isDualDecodeActive()) {
1769
+ throw new RemotePlayerError(6506, "Cannot call setVideoRect() when dual decode is not enabled");
1770
+ }
1771
+
1772
+ layer = layer ?? VideoLayer.FRONT;
1773
+
1774
+ const FCID = getFCID();
1775
+ const message = {
1776
+ type: "remotePlayer.setVideoRect",
1777
+ class: "remotePlayer",
1778
+ action: "setVideoRect",
1779
+ fcid: FCID,
1780
+ playbackId: this._playbackId,
1781
+ layer
1782
+ };
1783
+
1784
+ if (fullScreen) {
1785
+ message.fullScreen = true;
1786
+ } else {
1787
+ const winW = window.innerWidth;
1788
+ const winH = window.innerHeight;
1789
+ const isValid = [x, y, width, height].every((value) => typeof value === "number" && Number.isFinite(value) && value >= 0)
1790
+ && x + width <= winW && y + height <= winH;
1791
+ if (!isValid) {
1792
+ throw new RemotePlayerError(6505, `setVideoRect() received invalid rectangle {x:${x}, y:${y}, width:${width}, height:${height}} for window {width:${winW}, height:${winH}}`);
1793
+ }
1794
+ const { width: abrW, height: abrH } = getDualDecodeAbrSurfaceSize();
1795
+ message.x = Math.round(x * abrW / winW);
1796
+ message.y = Math.round(y * abrH / winH);
1797
+ message.width = Math.round(width * abrW / winW);
1798
+ message.height = Math.round(height * abrH / winH);
1799
+ }
1800
+
1801
+ if (window.cefQuery) {
1802
+ const logger = sdkLogger.withFields({ FCID });
1803
+ logger.log(`remotePlayer setVideoRect: sending setVideoRect action fullScreen=${!!fullScreen} layer=${layer}`);
1804
+ const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
1805
+ return new Promise((resolve, reject) => {
1806
+ let timerId = 0;
1807
+ const timeBeforeSendingRequest = Date.now();
1808
+ const queryId = window.cefQuery({
1809
+ request: JSON.stringify(request),
1810
+ persistent: false,
1811
+ onSuccess: () => {
1812
+ const duration = Date.now() - timeBeforeSendingRequest;
1813
+ logger.withFields({ duration }).log(`setVideoRect completed successfully after ${duration} ms`);
1814
+ timerId = clearTimer(timerId);
1815
+ resolve();
1816
+ },
1817
+ onFailure: (code, msg) => {
1818
+ const duration = Date.now() - timeBeforeSendingRequest;
1819
+ logger.withFields({ duration }).log(`setVideoRect failed after ${duration} ms. Error code: ${code}, error message: ${msg}`);
1820
+ timerId = clearTimer(timerId);
1821
+ reject(new RemotePlayerError(code, msg));
1822
+ }
1823
+ });
1824
+ logger.log(`window.cefQuery for setVideoRect returned query id ${queryId}`);
1825
+ const timeout = this._remotePlayerConfirmationTimeout + 1000;
1826
+ timerId = setTimeout(() => {
1827
+ logger.log(`setVideoRect reached timeout of ${timeout} ms, canceling query id ${queryId}`);
1828
+ window.cefQueryCancel(queryId);
1829
+ reject(new RemotePlayerError(6000, `setVideoRect reached timeout of ${timeout} ms`));
1830
+ }, timeout, queryId);
1831
+ });
1832
+ }
1833
+ sdkLogger.error("remotePlayer setVideoRect: window.cefQuery is undefined");
1834
+ return Promise.resolve(undefined);
1835
+ }
1836
+
1837
+ /** Make the remote player (ABR) video fullscreen on the given layer.
1838
+ * Only available when dual decode is enabled.
1839
+ * @param {VideoLayer} [layer=VideoLayer.FRONT] which layer the ABR video should be rendered on
1840
+ * @returns {Promise}
1841
+ * @throws {RemotePlayerError} error object contains code & msg
1842
+ */
1843
+ setVideoFullscreen(layer) {
1844
+ return this.setVideoRect({ fullScreen: true, layer: layer ?? VideoLayer.FRONT });
1845
+ }
1846
+
1657
1847
  /**
1658
1848
  * Getter/Setter for currentTime
1659
1849
  */
@@ -1763,16 +1953,24 @@ class RemotePlayer extends RemotePlayerInterface {
1763
1953
  this._pendingSeekPosition = playbackPosition;
1764
1954
  this._setPlaybackPositionAnchor(playbackPosition);
1765
1955
  // If the local player is just initializing after loading the manifest, ignore the seeking event.
1766
- if (this._localPlayerLoadCurrentTime === 0 && this._videoElement.currentTime > ONE_DAY_SECONDS) {
1767
- sdkLogger.info(`Seeking ignored for video currentTime init: currentTime: ${this._videoElement.currentTime}`);
1768
- this._localPlayerLoadCurrentTime = this._videoElement.currentTime;
1769
- // if the difference between the remote player and the video element current time is greater than the load start sync diff (default 1 second), seek to the remote player current time
1770
- if (this._isPlaying && this.currentTime > 0 && Math.abs(this._videoElement.currentTime - this.currentTime) > this._loadStartSyncDiff) {
1771
- sdkLogger.info(`Remote player is playing, seeking from ${this._videoElement.currentTime} to ${this.currentTime}`);
1772
- this._isSeekingByPlatform = true;
1773
- this._videoElement.currentTime = this.currentTime;
1956
+ // Live: Shaka jumps from 0 to a UTC wall-clock time (> one day in seconds).
1957
+ // VOD: load(url, startPosition) causes Shaka to seek near that start position (within epsilon).
1958
+ if (this._videoElement) {
1959
+ const currentTime = this._videoElement.currentTime;
1960
+ const isLiveInitSeek = this._localPlayerLoadCurrentTime === 0 && currentTime > ONE_DAY_SECONDS;
1961
+ const isVodInitSeek = this._localPlayerLoadCurrentTime === 0 && this._loadStartPosition > 0 && Math.abs(currentTime - this._loadStartPosition) <= VOD_INIT_SEEK_EPSILON_SEC;
1962
+ sdkLogger.info(`Check for init seek: currentTime: ${currentTime}, loadStartPosition: ${this._loadStartPosition}, isLiveInitSeek: ${isLiveInitSeek}, isVodInitSeek: ${isVodInitSeek}`);
1963
+ if (isLiveInitSeek || isVodInitSeek) {
1964
+ sdkLogger.info(`Seeking ignored for video currentTime init: currentTime: ${currentTime}, loadStartPosition: ${this._loadStartPosition}`);
1965
+ this._localPlayerLoadCurrentTime = currentTime;
1966
+ // if the difference between the remote player and the video element current time is greater than the load start sync diff (default 1 second), seek to the remote player current time
1967
+ if (isLiveInitSeek && this._isPlaying && this.currentTime > 0 && Math.abs(currentTime - this.currentTime) > this._loadStartSyncDiff) {
1968
+ sdkLogger.info(`Remote player is playing, seeking from ${currentTime} to ${this.currentTime}`);
1969
+ this._isSeekingByPlatform = true;
1970
+ this._videoElement.currentTime = this.currentTime;
1971
+ }
1972
+ return;
1774
1973
  }
1775
- return;
1776
1974
  }
1777
1975
  }
1778
1976
  const backgroundSeekSupported = isAppVersionAboveOrEqual(this._deviceAppVersion, backgroundSeekMinClientAppVersion);
@@ -1787,6 +1985,31 @@ class RemotePlayer extends RemotePlayerInterface {
1787
1985
  }
1788
1986
  }
1789
1987
 
1988
+ /**
1989
+ * Resolve the initial target playing state from current playback and lifecycle state.
1990
+ *
1991
+ * | _isPlaying | lifecycle.state | _inTransitionToForeground | Result |
1992
+ * |------------|-----------------------------|---------------------------|--------------|
1993
+ * | false | any | any | PAUSED |
1994
+ * | true | FOREGROUND | any | PLAYING_UI |
1995
+ * | true | BACKGROUND | true | PLAYING_UI |
1996
+ * | true | BACKGROUND | false | PLAYING_ABR |
1997
+ * | true | IN_TRANSITION_TO_BACKGROUND | any | PLAYING_ABR |
1998
+ *
1999
+ * @private
2000
+ * @returns {string} TargetPlayingState value
2001
+ */
2002
+ _getInitialTargetPlayingState() {
2003
+ if (!this._isPlaying) {
2004
+ return TargetPlayingState.PAUSED;
2005
+ }
2006
+ if (!lifecycle._inTransitionToForeground
2007
+ && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
2008
+ return TargetPlayingState.PLAYING_ABR;
2009
+ }
2010
+ return TargetPlayingState.PLAYING_UI;
2011
+ }
2012
+
1790
2013
  /** Performs the seek operation which comprises the following steps:
1791
2014
  * 1) Possible pause if playing
1792
2015
  * 2) Wait for any further changes in the currentTime
@@ -1799,16 +2022,7 @@ class RemotePlayer extends RemotePlayerInterface {
1799
2022
  * */
1800
2023
  async _atomicSeek() {
1801
2024
  sdkLogger.info("Seeking: local video element seeking start while isPlaying: ", this._isPlaying);
1802
- if (this._isPlaying) {
1803
- if (!(lifecycle._inTransitionToForeground || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND) && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
1804
- sdkLogger.info("seek in background", this._isPlaying);
1805
- this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
1806
- } else {
1807
- this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
1808
- }
1809
- } else {
1810
- this._targetSeekPlayingState = TargetPlayingState.PAUSED;
1811
- }
2025
+ this._targetSeekPlayingState = this._getInitialTargetPlayingState();
1812
2026
 
1813
2027
  // The platform could be currently syncing audio/video using playback rate. Reset when performing seek.
1814
2028
  if (this._videoElement) {
@@ -1901,7 +2115,7 @@ class RemotePlayer extends RemotePlayerInterface {
1901
2115
  return Promise.resolve();
1902
2116
  }
1903
2117
 
1904
- this._targetSetAudioPlayingState = this._isPlaying ? TargetPlayingState.PLAYING_UI : TargetPlayingState.PAUSED;
2118
+ this._targetSetAudioPlayingState = this._getInitialTargetPlayingState();
1905
2119
  sdkLogger.log(`remotePlayer _atomicSetAudioLanguage: prevAudioTrack=${prevSelectedAudioTrack} audioTrackId=${audioTrackId} isPlaying=${this._isPlaying} targetState=${this._targetSetAudioPlayingState}`);
1906
2120
 
1907
2121
  this._abortSetAudioLanguage = false;
@@ -1968,7 +2182,7 @@ class RemotePlayer extends RemotePlayerInterface {
1968
2182
  return Promise.resolve();
1969
2183
  }
1970
2184
 
1971
- this._targetSetSubtitlePlayingState = this._isPlaying ? TargetPlayingState.PLAYING_UI : TargetPlayingState.PAUSED;
2185
+ this._targetSetSubtitlePlayingState = this._getInitialTargetPlayingState();
1972
2186
  sdkLogger.log(`remotePlayer _atomicSetSubtitleLanguage: prevTextTrack=${prevSelectedTextTrack} textTrackId=${textTrackId} isPlaying=${this._isPlaying} targetState=${this._targetSetSubtitlePlayingState}`);
1973
2187
 
1974
2188
  this._abortSetSubtitleLanguage = false;