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/dist/bundle.js +1 -1
- package/dist/implementation.bundle.js +1 -1
- package/package.json +3 -1
- package/src/implementation/api.js +7 -5
- package/src/implementation/displayManager.js +7 -0
- package/src/implementation/lifecycle.js +105 -0
- package/src/implementation/remotePlayer.js +241 -27
- package/src/implementation/senzaShakaPlayer.js +22 -4
- package/src/implementation/utils.js +28 -1
- package/src/interface/overlay.js +0 -1
- package/src/interface/remotePlayer.js +44 -1
- package/src/interface/version.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "senza-sdk",
|
|
3
|
-
"version": "4.
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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.
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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;
|