senza-sdk 4.5.9 → 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.9",
3
+ "version": "4.6.0",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
@@ -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) {
@@ -29,6 +29,7 @@ const MULTI_SEEK_DELAY_MSEC = 250;
29
29
  const LOAD_START_SYNC_DIFF = 1;
30
30
  const INVALID_SET_PLAYABLE_URI_CODE = 99;
31
31
  export const ONE_DAY_SECONDS = 86400;
32
+ export const VOD_INIT_SEEK_EPSILON_SEC = 1;
32
33
  const BACKGROUND_LOAD_MIN_SDK_VERSION = "4.5.0";
33
34
 
34
35
  // Default timeout for waiting for the remote player confirmation
@@ -145,6 +146,14 @@ class RemotePlayer extends RemotePlayerInterface {
145
146
  */
146
147
  this._isSeekingByPlatform = false;
147
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
+
148
157
  /**
149
158
  * @type {boolean}
150
159
  * @description field indicating if the remote player is playing abr or not. Should be changed when starting/stoping playback
@@ -225,14 +234,18 @@ class RemotePlayer extends RemotePlayerInterface {
225
234
  this._autoTune = false;
226
235
 
227
236
  this._isPlaying = window?.sessionStorage?.getItem("senzaSdk_isPlaying") === "true";
237
+ this._textTrackVisibility = window?.sessionStorage?.getItem("senzaSdk_textTrackVisibility") === "true";
228
238
  if (this._isPlaying) {
229
239
  this._playbackPositionAnchor = getPlaybackInfo()?.playbackPosition || 0;
230
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;
231
244
  } else {
232
245
  this._stopPlaybackClock();
233
246
  }
234
247
 
235
- 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}`);
236
249
  this._loadedUrl = playerState?.playbackUrl || "";
237
250
  // Make sure that the event listeners are registered only once
238
251
  if (!this._isInitialized) {
@@ -601,6 +614,7 @@ class RemotePlayer extends RemotePlayerInterface {
601
614
  this._isSeekingByPlatform = false;
602
615
  this._isSeekingByApplication = false;
603
616
  this._isPlayingEventPending = false;
617
+ this._loadStartPosition = 0;
604
618
  this._stopPlaybackClock();
605
619
  this._playbackPositionAnchor = 0;
606
620
  }
@@ -949,6 +963,7 @@ class RemotePlayer extends RemotePlayerInterface {
949
963
  this._changePlayMode(false);
950
964
  return new Promise((resolve, reject) => {
951
965
  const playbackPosition = position || 0;
966
+ this._loadStartPosition = playbackPosition;
952
967
  if (reset) {
953
968
  // first, zero playback position
954
969
  setPlaybackInfo({ playbackPosition: 0 });
@@ -1641,14 +1656,33 @@ class RemotePlayer extends RemotePlayerInterface {
1641
1656
  // Convert to boolean in case apps pass 0/1 instead false/true.
1642
1657
  const newVisibility = !!visible;
1643
1658
 
1659
+ this._textTrackVisibility = newVisibility;
1660
+ window?.sessionStorage?.setItem("senzaSdk_textTrackVisibility", String(newVisibility));
1661
+
1644
1662
  if (oldVisibility === newVisibility) {
1645
1663
  return;
1646
1664
  }
1647
1665
 
1648
- this._textTrackVisibility = newVisibility;
1649
- if (!this._textTrackVisibility) {
1650
- // Setting the visibility to false clears any previous selections user has done
1651
- 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");
1652
1686
  }
1653
1687
  }
1654
1688
 
@@ -1919,16 +1953,24 @@ class RemotePlayer extends RemotePlayerInterface {
1919
1953
  this._pendingSeekPosition = playbackPosition;
1920
1954
  this._setPlaybackPositionAnchor(playbackPosition);
1921
1955
  // If the local player is just initializing after loading the manifest, ignore the seeking event.
1922
- if (this._localPlayerLoadCurrentTime === 0 && this._videoElement.currentTime > ONE_DAY_SECONDS) {
1923
- sdkLogger.info(`Seeking ignored for video currentTime init: currentTime: ${this._videoElement.currentTime}`);
1924
- this._localPlayerLoadCurrentTime = this._videoElement.currentTime;
1925
- // 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
1926
- if (this._isPlaying && this.currentTime > 0 && Math.abs(this._videoElement.currentTime - this.currentTime) > this._loadStartSyncDiff) {
1927
- sdkLogger.info(`Remote player is playing, seeking from ${this._videoElement.currentTime} to ${this.currentTime}`);
1928
- this._isSeekingByPlatform = true;
1929
- 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;
1930
1973
  }
1931
- return;
1932
1974
  }
1933
1975
  }
1934
1976
  const backgroundSeekSupported = isAppVersionAboveOrEqual(this._deviceAppVersion, backgroundSeekMinClientAppVersion);
@@ -1943,6 +1985,31 @@ class RemotePlayer extends RemotePlayerInterface {
1943
1985
  }
1944
1986
  }
1945
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
+
1946
2013
  /** Performs the seek operation which comprises the following steps:
1947
2014
  * 1) Possible pause if playing
1948
2015
  * 2) Wait for any further changes in the currentTime
@@ -1955,16 +2022,7 @@ class RemotePlayer extends RemotePlayerInterface {
1955
2022
  * */
1956
2023
  async _atomicSeek() {
1957
2024
  sdkLogger.info("Seeking: local video element seeking start while isPlaying: ", this._isPlaying);
1958
- if (this._isPlaying) {
1959
- if (!(lifecycle._inTransitionToForeground || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND) && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
1960
- sdkLogger.info("seek in background", this._isPlaying);
1961
- this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
1962
- } else {
1963
- this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
1964
- }
1965
- } else {
1966
- this._targetSeekPlayingState = TargetPlayingState.PAUSED;
1967
- }
2025
+ this._targetSeekPlayingState = this._getInitialTargetPlayingState();
1968
2026
 
1969
2027
  // The platform could be currently syncing audio/video using playback rate. Reset when performing seek.
1970
2028
  if (this._videoElement) {
@@ -2057,7 +2115,7 @@ class RemotePlayer extends RemotePlayerInterface {
2057
2115
  return Promise.resolve();
2058
2116
  }
2059
2117
 
2060
- this._targetSetAudioPlayingState = this._isPlaying ? TargetPlayingState.PLAYING_UI : TargetPlayingState.PAUSED;
2118
+ this._targetSetAudioPlayingState = this._getInitialTargetPlayingState();
2061
2119
  sdkLogger.log(`remotePlayer _atomicSetAudioLanguage: prevAudioTrack=${prevSelectedAudioTrack} audioTrackId=${audioTrackId} isPlaying=${this._isPlaying} targetState=${this._targetSetAudioPlayingState}`);
2062
2120
 
2063
2121
  this._abortSetAudioLanguage = false;
@@ -2124,7 +2182,7 @@ class RemotePlayer extends RemotePlayerInterface {
2124
2182
  return Promise.resolve();
2125
2183
  }
2126
2184
 
2127
- this._targetSetSubtitlePlayingState = this._isPlaying ? TargetPlayingState.PLAYING_UI : TargetPlayingState.PAUSED;
2185
+ this._targetSetSubtitlePlayingState = this._getInitialTargetPlayingState();
2128
2186
  sdkLogger.log(`remotePlayer _atomicSetSubtitleLanguage: prevTextTrack=${prevSelectedTextTrack} textTrackId=${textTrackId} isPlaying=${this._isPlaying} targetState=${this._targetSetSubtitlePlayingState}`);
2129
2187
 
2130
2188
  this._abortSetSubtitleLanguage = false;
@@ -1,7 +1,7 @@
1
1
  import { SenzaShakaPlayer as SenzaShakaInterface, shaka } from "../interface/senzaShakaPlayer";
2
2
  import * as shakaDebug from "shaka-player/dist/shaka-player.compiled.debug.js";
3
3
  import { remotePlayer, lifecycle, getPlatformInfo } from "./api";
4
- import { ONE_DAY_SECONDS } from "./remotePlayer.js";
4
+ import { ONE_DAY_SECONDS, VOD_INIT_SEEK_EPSILON_SEC } from "./remotePlayer.js";
5
5
  import { sdkLogger, SwitchMode } from "./utils";
6
6
  import moment from "moment";
7
7
  import set from "lodash.set";
@@ -156,12 +156,19 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
156
156
  _seekCounterAfterLoad = 0;
157
157
 
158
158
  /**
159
- * When true, the next `seeked` is Shaka jumping to live-edge time after load; skip background pause logic.
159
+ * When true, the next `seeked` is Shaka jumping to live-edge / VOD startTime after load; skip background pause logic.
160
160
  * @private
161
161
  * @type {boolean}
162
162
  */
163
163
  _ignoreNextSeekedForInit = false;
164
164
 
165
+ /**
166
+ * Start position (seconds) from the most recent load(). Used to ignore Shaka's VOD init seeking/seeked.
167
+ * @private
168
+ * @type {number}
169
+ */
170
+ _loadStartPosition = 0;
171
+
165
172
  /**
166
173
  * Whether the player should call lifecycle.moveToForeground on pause, remote ended/error, unload, and destroy.
167
174
  * Only when auto-background is enabled (including streamer overrides) and the UI moved to background via the
@@ -185,8 +192,16 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
185
192
  this._ignoreNextSeekedForInit = false;
186
193
  },
187
194
  "seeking": () => {
188
- // If the first seeking after load started from time 0, and the current time is a UTC time (live asset), ignore the next seeked event to avoid background pause logic.
189
- if (this._seekCounterAfterLoad < 1 && this._loadStartedAtZero && this.videoElement.currentTime > ONE_DAY_SECONDS) {
195
+ // Ignore the first seeking after load when it is Shaka's initialization jump:
196
+ // - Live: currentTime is a UTC wall-clock time (> one day in seconds)
197
+ // - VOD: currentTime is within epsilon of this._loadStartPosition
198
+ const isFirstSeekAfterLoadAtZero = this._seekCounterAfterLoad < 1 && this._loadStartedAtZero;
199
+ const isLiveInitSeek = isFirstSeekAfterLoadAtZero && this.videoElement.currentTime > ONE_DAY_SECONDS;
200
+ const isVodInitSeek = isFirstSeekAfterLoadAtZero
201
+ && this._loadStartPosition > 0
202
+ && Math.abs(this.videoElement.currentTime - this._loadStartPosition) <= VOD_INIT_SEEK_EPSILON_SEC;
203
+ if (isLiveInitSeek || isVodInitSeek) {
204
+ sdkLogger.info("senzaShakaPlayer seeking: ignoring next seeked for init");
190
205
  this._ignoreNextSeekedForInit = true;
191
206
  } else {
192
207
  // Another seeking before seeked means this is no longer the load initialization jump.
@@ -820,6 +835,9 @@ export class SenzaShakaPlayer extends SenzaShakaInterface {
820
835
  startTime = -minInitialLiveOffset;
821
836
  }
822
837
 
838
+ // Used to ignore Shaka's init seeking/seeked when currentTime matches load start.
839
+ this._loadStartPosition = startTime ?? 0;
840
+
823
841
  // This callback will be activated when the manifest is loaded. It will trigger load of the remote player.
824
842
  // This will ensure that the remote player is loaded only after the manifest is loaded by local player.
825
843
  const responseFilterCallback = async (type, response) => {
@@ -10,7 +10,6 @@ import { noop } from "./utils.js";
10
10
  /**
11
11
  * Overlay is a singleton class that manages displaying UI frames as image overlays. The image can be displayed over background video playback.
12
12
  * It can be used in any lifecycle state.
13
- * @private
14
13
  * @fires error
15
14
  */
16
15
  class Overlay extends EventTarget {
@@ -1 +1 @@
1
- export const version = "4.5.9";
1
+ export const version = "4.6.0";