senza-sdk 4.2.65-90c49ac.0 → 4.2.65-a3f8c5a.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.
@@ -51,3 +51,7 @@
51
51
  Copyright 2015 Tobias Nickel
52
52
  SPDX-License-Identifier: MIT
53
53
  */
54
+
55
+ //! moment.js
56
+
57
+ //! moment.js locale configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senza-sdk",
3
- "version": "4.2.65-90c49ac.0",
3
+ "version": "4.2.65-a3f8c5a.0",
4
4
  "main": "./src/api.js",
5
5
  "description": "API for Senza application",
6
6
  "license": "MIT",
@@ -50,6 +50,7 @@
50
50
  }
51
51
  },
52
52
  "dependencies": {
53
- "shaka-player": "^4.12.5"
53
+ "shaka-player": "^4.12.5",
54
+ "moment": "^2.30.1"
54
55
  }
55
56
  }
@@ -204,6 +204,7 @@ const setupSequence = (components, items) => {
204
204
  };
205
205
  methodInject(components, inject, (name) => name.startsWith("_") || name.startsWith("get"));
206
206
  handleKeys(items);
207
+ playersEvents(components, items);
207
208
  sdkLogger.log("Sequence initialized.");
208
209
  };
209
210
 
@@ -257,3 +258,37 @@ export const showSequence = (visible = true) => {
257
258
  }
258
259
  container.style.visibility = visible ? "visible" : "hidden";
259
260
  };
261
+ function playersEvents(components, items) {
262
+ components.remotePlayer.addEventListener("videoelementattached", () => {
263
+ const video = components.remotePlayer._videoElement;
264
+ if (video && !video._devSequenceRateChangeHandler) {
265
+ video._devSequenceRateChangeHandler = () => {
266
+ items.push({
267
+ component: "localPlayer",
268
+ id: "ratechange=" + video.playbackRate,
269
+ time: performance.now() - currentTime
270
+ });
271
+ };
272
+ video.addEventListener("ratechange", video._devSequenceRateChangeHandler);
273
+ }
274
+
275
+ if (video && !video._devSequencePlayingHandler) {
276
+ video._devSequencePlayingHandler = () => {
277
+ items.push({
278
+ component: "localPlayer",
279
+ id: "playing event",
280
+ time: performance.now() - currentTime
281
+ });
282
+ };
283
+ video.addEventListener("playing", video._devSequencePlayingHandler);
284
+ }
285
+ });
286
+ components.remotePlayer.addEventListener("playing", () => {
287
+ items.push({
288
+ component: "remotePlayer",
289
+ id: "playing event",
290
+ time: performance.now() - currentTime
291
+ });
292
+ });
293
+ }
294
+
package/src/lifecycle.js CHANGED
@@ -102,9 +102,7 @@ class Lifecycle extends EventTarget {
102
102
  * @private
103
103
  */
104
104
  this._isInitialized = false;
105
- this._inTransitionToForeground = false;
106
- this._inTransitionToBackground = false;
107
- this._inTransitionToStandby = false;
105
+ this._inTransition = false;
108
106
 
109
107
  /**
110
108
  * Event listeners manager for the userdisconnected event
@@ -386,7 +384,7 @@ class Lifecycle extends EventTarget {
386
384
  // This api is part of epic HSDEV-713
387
385
  _moveToUiStandby() {
388
386
  if (window.cefQuery) {
389
- this._inTransitionToStandby = true;
387
+ this._inTransition = true;
390
388
  return new Promise((resolve, reject) => {
391
389
  const FCID = getFCID();
392
390
  const request = { target: "TC", waitForResponse: false, internalAction: "uiExit", message: JSON.stringify({ type: "uiStandbyRequest", fcid: FCID }) };
@@ -397,12 +395,12 @@ class Lifecycle extends EventTarget {
397
395
  persistent: false,
398
396
  onSuccess: () => {
399
397
  logger.log("[ moveToUiStandby ] moveToUiStandby successfully sent");
400
- this._inTransitionToStandby = false;
398
+ this._inTransition = false;
401
399
  resolve(true);
402
400
  },
403
401
  onFailure: (code, msg) => {
404
402
  logger.error(`[ moveToUiStandby ] moveToUiStandby failed: ${code} ${msg}`);
405
- this._inTransitionToStandby = false;
403
+ this._inTransition = false;
406
404
  reject(`moveToUiStandby failed: ${code} ${msg}`);
407
405
  }
408
406
  });
@@ -536,14 +534,6 @@ class Lifecycle extends EventTarget {
536
534
  this._countdown = null;
537
535
  }
538
536
 
539
- /**
540
- * @private
541
- */
542
- _isInTransition() {
543
- return this._inTransitionToForeground || this._inTransitionToBackground || this._inTransitionToStandby;
544
- }
545
-
546
-
547
537
  /**
548
538
  * @deprecated use lifecycle.state instead.
549
539
  * Async function that returns the ui lifecycle state
@@ -586,19 +576,14 @@ class Lifecycle extends EventTarget {
586
576
  */
587
577
  moveToForeground() {
588
578
  if (window.cefQuery) {
589
- const inTransition = this._isInTransition();
590
- if (inTransition || this._state === this.UiState.FOREGROUND || this._state === this.UiState.IN_TRANSITION_TO_FOREGROUND) {
591
- sdkLogger.warn(`lifecycle moveToForeground: No need to transition to foreground, state: ${this._state} transition: ${inTransition}`);
579
+ if (this._inTransition || this._state === this.UiState.FOREGROUND || this._state === this.UiState.IN_TRANSITION_TO_FOREGROUND) {
580
+ sdkLogger.warn(`lifecycle moveToForeground: No need to transition to foreground, state: ${this._state} transition: ${this._inTransition}`);
592
581
  return Promise.resolve(false);
593
582
  }
594
- this._inTransitionToForeground = true;
583
+ this._inTransition = true;
595
584
  alarmManager._moveToForegroundCalled();
596
585
  const FCID = getFCID();
597
586
  if (this._remotePlayerApiVersion >= 2) {
598
- // 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.
599
- if (remotePlayer._isSeekingByApplication && remotePlayer._targetSeekPlayingState === TargetPlayingState.PLAYING_ABR) {
600
- remotePlayer._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
601
- }
602
587
  return new Promise((resolve, reject) => {
603
588
  const FCID = getFCID();
604
589
  const logger = sdkLogger.withFields({ FCID });
@@ -618,14 +603,14 @@ class Lifecycle extends EventTarget {
618
603
  onSuccess: () => {
619
604
  const duration = Date.now() - timeBeforeSendingRequest;
620
605
  logger.withFields({ duration }).log(`stop completed successfully after ${duration} ms`);
621
- this._inTransitionToForeground = false;
606
+ this._inTransition = false;
622
607
  timerId = clearTimer(timerId);
623
608
  resolve(true);
624
609
  },
625
610
  onFailure: (code, msg) => {
626
611
  const duration = Date.now() - timeBeforeSendingRequest;
627
612
  logger.withFields({ duration }).log(`stop failed after ${duration} ms. Error code: ${code}, error message: ${msg}`);
628
- this._inTransitionToForeground = false;
613
+ this._inTransition = false;
629
614
  timerId = clearTimer(timerId);
630
615
  reject(new SenzaError(code, msg));
631
616
  }
@@ -634,7 +619,7 @@ class Lifecycle extends EventTarget {
634
619
  const timeout = this._remotePlayerConfirmationTimeout + 1000;
635
620
  timerId = setTimeout(() => {
636
621
  logger.log(`stop reached timeout of ${timeout} ms, canceling query id ${queryId}`);
637
- this._inTransitionToForeground = false;
622
+ this._inTransition = false;
638
623
  window.cefQueryCancel(queryId);
639
624
  reject(new SenzaError(6000, `stop reached timeout of ${timeout} ms`));
640
625
  }, timeout, queryId);
@@ -649,11 +634,11 @@ class Lifecycle extends EventTarget {
649
634
  persistent: false,
650
635
  onSuccess: () => {
651
636
  logger.log("uiActiveRequest successfully sent");
652
- this._inTransitionToForeground = false;
637
+ this._inTransition = false;
653
638
  resolve(true);
654
639
  },
655
640
  onFailure: (code, msg) => {
656
- this._inTransitionToForeground = false;
641
+ this._inTransition = false;
657
642
  logger.error(`uiActiveRequest failed: ${code} ${msg}`);
658
643
  reject(`uiActiveRequest failed: ${code} ${msg}`);
659
644
  }
@@ -666,6 +651,10 @@ class Lifecycle extends EventTarget {
666
651
 
667
652
  _moveToBackground() {
668
653
  if (window.cefQuery) {
654
+ if (this._inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
655
+ sdkLogger.warn(`lifecycle moveToBackground: No need to transition to background, state: ${this._state} transition: ${this._inTransition}`);
656
+ return Promise.resolve(false);
657
+ }
669
658
  // If audio sync is disabled, we only need to sync before remote player starts playing
670
659
  if (!isAudioSyncConfigured()) {
671
660
  remotePlayer._syncRemotePlayerWithLocalPlayer();
@@ -677,7 +666,7 @@ class Lifecycle extends EventTarget {
677
666
  return this._moveToUiStandby();
678
667
  }
679
668
 
680
- this._inTransitionToBackground = true;
669
+ this._inTransition = true;
681
670
  return new Promise((resolve, reject) => {
682
671
  const FCID = getFCID();
683
672
  const logger = sdkLogger.withFields({ FCID });
@@ -718,14 +707,14 @@ class Lifecycle extends EventTarget {
718
707
  onSuccess: () => {
719
708
  const duration = Date.now() - timeBeforeSendingRequest;
720
709
  logger.withFields({ duration }).log(`play completed successfully after ${duration} ms`);
721
- this._inTransitionToBackground = false;
710
+ this._inTransition = false;
722
711
  timerId = clearTimer(timerId);
723
712
  resolve();
724
713
  },
725
714
  onFailure: (code, msg) => {
726
715
  const duration = Date.now() - timeBeforeSendingRequest;
727
716
  logger.withFields({ duration }).log(`play failed after ${duration} ms. Error code: ${code}, error message: ${msg}`);
728
- this._inTransitionToBackground = false;
717
+ this._inTransition = false;
729
718
  timerId = clearTimer(timerId);
730
719
  reject(new SenzaError(code, msg));
731
720
  }
@@ -735,7 +724,7 @@ class Lifecycle extends EventTarget {
735
724
  const timeout = this._remotePlayerConfirmationTimeout + 1000;
736
725
  timerId = setTimeout(() => {
737
726
  logger.log(`play reached timeout of ${timeout} ms, canceling query id ${queryId}`);
738
- this._inTransitionToBackground = false;
727
+ this._inTransition = false;
739
728
  window.cefQueryCancel(queryId);
740
729
  reject(new SenzaError(6000, `play reached timeout of ${timeout} ms`));
741
730
  }, timeout, queryId);
@@ -759,11 +748,11 @@ class Lifecycle extends EventTarget {
759
748
  */
760
749
  moveToBackground() {
761
750
  if (window.cefQuery) {
762
- const inTransition = this._isInTransition();
763
- if (inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
764
- sdkLogger.warn(`lifecycle moveToBackground: No need to transition to background, state: ${this._state} transition: ${inTransition}`);
751
+ if (this._inTransition || this._state === this.UiState.BACKGROUND || this._state === this.UiState.IN_TRANSITION_TO_BACKGROUND) {
752
+ sdkLogger.warn(`lifecycle moveToBackground: No need to transition to background, state: ${this._state} transition: ${this._inTransition}`);
765
753
  return Promise.resolve(false);
766
754
  }
755
+
767
756
  if (remotePlayer._isSeekingByApplication) {
768
757
  remotePlayer._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
769
758
  return Promise.resolve(true);
@@ -795,6 +784,7 @@ class Lifecycle extends EventTarget {
795
784
  }
796
785
 
797
786
  // TODO: Need to discuss checking for a valid tenantId in the Senza platform
787
+ remotePlayer._blackoutTime = undefined;
798
788
  const contentHubTenantId = getPlatformInfo().sessionInfo?.homeSessionInfo?.tenantInfo?.contentHubTenantId;
799
789
  const homeTenantId = getPlatformInfo().sessionInfo?.homeSessionInfo?.tenantId;
800
790
  sdkLogger.log(`SwitchTenant for ${tenantId}`);
@@ -68,6 +68,7 @@ function setPlaybackInfo(playbackInfo) {
68
68
  * @typedef {Object} Config
69
69
  * @property {string} preferredAudioLanguage
70
70
  * @property {string} preferredSubtitlesLanguage
71
+ * @property {number} minSuggestedPresentationDelay - minimal delay allowed for live playback in seconds
71
72
  * @property {boolean} autoPlay - (Not implemented yet) upon loading start playing automatically
72
73
  */
73
74
 
@@ -78,9 +79,9 @@ function setPlaybackInfo(playbackInfo) {
78
79
  * @fires ended
79
80
  * @fires error
80
81
  * @fires onloadmodechange
82
+ * @fires playing
81
83
  * @fires seeking (Not implemented yet)
82
84
  * @fires seeked (Not implemented yet)
83
- * @fires loadedmetadata (Not implemented yet)
84
85
  */
85
86
  class RemotePlayer extends EventTarget {
86
87
  constructor() {
@@ -91,7 +92,8 @@ class RemotePlayer extends EventTarget {
91
92
  */
92
93
  this._config = {
93
94
  preferredAudioLanguage: "",
94
- preferredSubtitlesLanguage: ""
95
+ preferredSubtitlesLanguage: "",
96
+ minSuggestedPresentationDelay: 0
95
97
  };
96
98
  /**
97
99
  * @type {string}
@@ -161,6 +163,13 @@ class RemotePlayer extends EventTarget {
161
163
  */
162
164
  this._isPlaying = false;
163
165
 
166
+ /**
167
+ * @type {string}
168
+ * @description the last set blackout time as epoch seconds.
169
+ * @private
170
+ */
171
+ this._blackoutTime = undefined;
172
+
164
173
  /**
165
174
  * @event RemotePlayer#canplay
166
175
  * @description canplay event will be dispatched when the remote player can start play the event
@@ -524,10 +533,10 @@ class RemotePlayer extends EventTarget {
524
533
 
525
534
  /** setting values for properties in the player configuration using an object.
526
535
  * If the config does not support a property this is a no-op.
527
- * @param {Object} props the object with all the different properties to change
536
+ * @param {Config} props the object with all the different properties to change.
528
537
  * @example
529
538
  * remotePlayer.configure({ preferredAudioLanguage: 'en-US' })
530
- *
539
+ * remotePlayer.configure({ minSuggestedPresentationDelay: 6 })
531
540
  * */
532
541
  configure(props) {
533
542
  Object.entries(props).forEach(([key, value]) => {
@@ -896,6 +905,9 @@ class RemotePlayer extends EventTarget {
896
905
  this._updateSeekListeners(video);
897
906
  }
898
907
  this._videoElement = video;
908
+
909
+ // Emit a custom event to notify about the attachment of the video element
910
+ this.dispatchEvent(new Event("videoelementattached"));
899
911
  }
900
912
  }
901
913
 
@@ -911,7 +923,7 @@ class RemotePlayer extends EventTarget {
911
923
  /** Tell the remote player to load the given URL.
912
924
  * @param {string} url url to load
913
925
  * @param {number} [position] start position in seconds (if not provided, start from beginning (VOD) or current time (LTV))
914
- * @returns {Promise}
926
+ * @returns {Promise}
915
927
  * @throws {RemotePlayerError} error object contains code & msg
916
928
  *
917
929
  * */
@@ -946,6 +958,7 @@ class RemotePlayer extends EventTarget {
946
958
  this._abortSetAudioLanguage = true;
947
959
  this._abortSetSubtitleLanguage = true;
948
960
  this._abortSeeking = true;
961
+ this._blackoutTime = undefined;
949
962
  if (reset) {
950
963
  this._reset();
951
964
  }
@@ -976,6 +989,11 @@ class RemotePlayer extends EventTarget {
976
989
  message.action = "load";
977
990
  message.audioLanguage = audioLanguage;
978
991
  message.subtitlesLanguage = subtitlesLanguage;
992
+ if (this.getConfiguration().minSuggestedPresentationDelay > 0) {
993
+ message.cloudPlayerParams = {
994
+ "mspd": this.getConfiguration().minSuggestedPresentationDelay
995
+ };
996
+ }
979
997
  } else {
980
998
  message.type = "setPlayableUri";
981
999
  }
@@ -1133,13 +1151,8 @@ class RemotePlayer extends EventTarget {
1133
1151
 
1134
1152
  // If seeking in progress, wait for seek to complete before playing
1135
1153
  if (this._isSeekingByApplication) {
1136
- if (lifecycle.state === lifecycle.UiState.FOREGROUND) {
1137
- sdkLogger.info("application requesting play during seek. setting targetSeekPlayingState to PLAYING_UI");
1138
- this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
1139
- } else {
1140
- sdkLogger.info("application requesting play during seek. setting targetSeekPlayingState to PLAYING_ABR");
1141
- this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
1142
- }
1154
+ sdkLogger.info("application requesting play during seek");
1155
+ this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
1143
1156
  return Promise.resolve(true);
1144
1157
  }
1145
1158
  /*
@@ -1557,6 +1570,62 @@ class RemotePlayer extends EventTarget {
1557
1570
  }
1558
1571
  }
1559
1572
 
1573
+ setBlackoutTime(blackoutTime) {
1574
+
1575
+ if (!this._isInitialized) {
1576
+ throw new RemotePlayerError(6500, "Cannot call setBlackoutTime() if remote player is not initialized");
1577
+ }
1578
+
1579
+ if (this._loadMode !== this.LoadMode.LOADED) {
1580
+ throw new RemotePlayerError(6001, "Cannot call setBlackoutTime() if player is not loaded");
1581
+ }
1582
+
1583
+ if (window.cefQuery) {
1584
+ const FCID = getFCID();
1585
+ const logger = sdkLogger.withFields({ FCID });
1586
+ logger.log("remotePlayer setBlackoutTime: sending screenBlackout action");
1587
+ const message = {
1588
+ type: "remotePlayer.screenBlackout",
1589
+ class: "remotePlayer",
1590
+ action: "screenBlackout",
1591
+ fcid: FCID,
1592
+ blackoutTime
1593
+ };
1594
+ const request = { target: "TC", waitForResponse: true, message: JSON.stringify(message) };
1595
+ return new Promise((resolve, reject) => {
1596
+ let timerId = 0;
1597
+ const timeBeforeSendingRequest = Date.now();
1598
+ const queryId = window.cefQuery({
1599
+ request: JSON.stringify(request),
1600
+ persistent: false,
1601
+ onSuccess: () => {
1602
+ this._blackoutTime = blackoutTime;
1603
+ const duration = Date.now() - timeBeforeSendingRequest;
1604
+ logger.withFields({ duration }).log(`setBlackoutTime completed successfully after ${duration} ms`);
1605
+ timerId = clearTimer(timerId);
1606
+ resolve();
1607
+ },
1608
+ onFailure: (code, msg) => {
1609
+ const duration = Date.now() - timeBeforeSendingRequest;
1610
+ logger.withFields({ duration }).log(`setBlackoutTime failed after ${duration} ms. Error code: ${code}, error message: ${msg}`);
1611
+ timerId = clearTimer(timerId);
1612
+ reject(new RemotePlayerError(code, msg));
1613
+ }
1614
+ });
1615
+
1616
+ logger.log(`window.cefQuery for setBlackoutTime returned query id ${queryId}`);
1617
+ const timeout = this._remotePlayerConfirmationTimeout + 1000;
1618
+ timerId = setTimeout(() => {
1619
+ logger.log(`setBlackoutTime reached timeout of ${timeout} ms, canceling query id ${queryId}`);
1620
+ window.cefQueryCancel(queryId);
1621
+ reject(new RemotePlayerError(6000, `setBlackoutTime reached timeout of ${timeout} ms`));
1622
+ }, timeout, queryId);
1623
+ });
1624
+ }
1625
+ sdkLogger.error("remotePlayer setBlackoutTime: window.cefQuery is undefined");
1626
+ return Promise.resolve(undefined);
1627
+ }
1628
+
1560
1629
  /**
1561
1630
  * Getter/Setter for currentTime
1562
1631
  */
@@ -1661,7 +1730,9 @@ class RemotePlayer extends EventTarget {
1661
1730
  }
1662
1731
  }
1663
1732
 
1664
- if (this._remotePlayerApiVersion >= 2 && !this._isSeekingByPlatform && !this._isSeekingByApplication) {
1733
+ // Only allow seeking in foreground. Still ignore the initialized local player seeking event above
1734
+ if (this._remotePlayerApiVersion >= 2 && !this._isSeekingByPlatform && !this._isSeekingByApplication &&
1735
+ (lifecycle.state === lifecycle.UiState.FOREGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_FOREGROUND)) {
1665
1736
  this._atomicSeek();
1666
1737
  } else {
1667
1738
  sdkLogger.info(`Seeking: skipping seeking event to currentTime: ${playbackPosition}, internalSeek: ${this._isSeekingByPlatform}, localPlayerSeek: ${this._isSeekingByApplication}, state: ${lifecycle.state}`);
@@ -1680,16 +1751,16 @@ class RemotePlayer extends EventTarget {
1680
1751
  * */
1681
1752
  async _atomicSeek() {
1682
1753
  sdkLogger.info("Seeking: local video element seeking start while isPlaying: ", this._isPlaying);
1683
- if (this._isPlaying) {
1684
- if (!lifecycle._inTransitionToForeground && (lifecycle._inTransitionToBackground || lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
1685
- sdkLogger.info("seek in background", this._isPlaying);
1686
- this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
1687
- } else {
1688
- this._targetSeekPlayingState = TargetPlayingState.PLAYING_UI;
1689
- }
1690
- } else {
1691
- this._targetSeekPlayingState = TargetPlayingState.PAUSED;
1692
- }
1754
+
1755
+ // Initialize the target playing state unless changed during the seek process
1756
+ // In the future, we should allow for seeking in background. Currently, there's no
1757
+ // way to know when the web application will call moveToForeground (i.e Before/After seek)
1758
+ // Therefore, for now, we will assume the target is either paused or playing in ui unless
1759
+ // specifically receiving a moveToBackground during the process.
1760
+ // if (this._isPlaying && (lifecycle.state === lifecycle.UiState.BACKGROUND || lifecycle.state === lifecycle.UiState.IN_TRANSITION_TO_BACKGROUND)) {
1761
+ // this._targetSeekPlayingState = TargetPlayingState.PLAYING_ABR;
1762
+ // }
1763
+ this._targetSeekPlayingState = this._isPlaying ? TargetPlayingState.PLAYING_UI : TargetPlayingState.PAUSED;
1693
1764
 
1694
1765
  // The platform could be currently syncing audio/video using playback rate. Reset when performing seek.
1695
1766
  if (this._videoElement) {
@@ -1756,7 +1827,7 @@ class RemotePlayer extends EventTarget {
1756
1827
 
1757
1828
  // If in TargetPlayingState.PAUSE, no need to resume.
1758
1829
  // Resume without awaiting to avoid blocking the seek process anymore
1759
- // In case where we aborted (new load or unload called), we don't want to resume playback.
1830
+ // In case where we aborted, we don't want to resume playback.
1760
1831
  if (!this._abortSeeking) {
1761
1832
  if (this._targetSeekPlayingState === TargetPlayingState.PLAYING_UI) {
1762
1833
  if (!this._isAudioSyncEnabled()) {
@@ -1765,8 +1836,6 @@ class RemotePlayer extends EventTarget {
1765
1836
  // resume audio play only if _isAudioSyncEnabled
1766
1837
  this._play(StreamType.AUDIO);
1767
1838
  } else if (this._targetSeekPlayingState === TargetPlayingState.PLAYING_ABR) {
1768
- // When moving back to background, we need to put the remote player back into play mode
1769
- this._changePlayMode(true);
1770
1839
  lifecycle._moveToBackground();
1771
1840
  }
1772
1841
  }
@@ -1,6 +1,8 @@
1
1
  import * as shaka from "shaka-player";
2
2
  import { remotePlayer, lifecycle, getPlatformInfo } from "./api";
3
3
  import { sdkLogger, iso6393to1 } from "./utils";
4
+ import moment from "moment";
5
+
4
6
 
5
7
  // Define custom error category
6
8
  shaka.util.Error.Category.SENZA_PLAYER_ERROR = 50;
@@ -76,7 +78,7 @@ export class SenzaShakaPlayer extends shaka.Player {
76
78
  * @private
77
79
  * @type {number}
78
80
  * @description Timeout in milliseconds to wait for playing event
79
- * @default 3000
81
+ * @default 4000
80
82
  */
81
83
  _playingTimeout = 4000;
82
84
 
@@ -305,6 +307,45 @@ export class SenzaShakaPlayer extends shaka.Player {
305
307
 
306
308
  }
307
309
 
310
+ /**
311
+ * @private
312
+ * @type {number}
313
+ * @description Minimum suggested presentation delay in seconds
314
+ * @default 15
315
+ */
316
+ _minSuggestedPresentationDelay = 15;
317
+
318
+ /**
319
+ * Modifies the suggestedPresentationDelay in the manifest text
320
+ * @private
321
+ * @param {string} manifestText - The MPD manifest text
322
+ * @returns {string} - Modified manifest text , or undefined if no modification was done
323
+ */
324
+ _updateManifestDelayIfBelowMinimum(manifestText) {
325
+ // Look for suggestedPresentationDelay attribute
326
+ const match = manifestText.match(/suggestedPresentationDelay="([^"]+)"/);
327
+ if (match) {
328
+ const durationString = match[1];
329
+ const duration = moment.duration(durationString);
330
+ const currentDelay = duration.asSeconds();
331
+
332
+ sdkLogger.info(`Found suggestedPresentationDelay in manifest: ${currentDelay.toFixed(3)}s`);
333
+
334
+ if (currentDelay < this._minSuggestedPresentationDelay) {
335
+ // Replace the value in the manifest text with 3 decimal places
336
+ manifestText = manifestText.replace(
337
+ /suggestedPresentationDelay="[^"]+"/,
338
+ `suggestedPresentationDelay="PT${this._minSuggestedPresentationDelay.toFixed(3)}S"`
339
+ );
340
+ sdkLogger.info(`Updated manifest suggestedPresentationDelay to ${this._minSuggestedPresentationDelay.toFixed(3)}s`);
341
+ return manifestText;
342
+ }
343
+ } else {
344
+ sdkLogger.info("suggestedPresentationDelay is not defined at the manifest");
345
+ }
346
+ return undefined;
347
+ }
348
+
308
349
  /**
309
350
  * Creates an instance of SenzaShakaPlayer, which is a subclass of shaka.Player.
310
351
  *
@@ -332,14 +373,38 @@ export class SenzaShakaPlayer extends shaka.Player {
332
373
  const playTimeout = getPlatformInfo()?.sessionInfo?.settings?.["ui-streamer"]?.playingEventTimeout;
333
374
  this._playingTimeout = (playTimeout >= 0) ? playTimeout*1000 : this._playingTimeout;
334
375
 
376
+ // Initialize minSuggestedPresentationDelay from UI settings or use default
377
+ const uiSettings = getPlatformInfo()?.sessionInfo?.settings?.["ui-streamer"];
378
+ if (uiSettings?.minSuggestedPresentationDelay !== undefined) {
379
+ this._minSuggestedPresentationDelay = uiSettings.minSuggestedPresentationDelay;
380
+ sdkLogger.info(`Using configured minSuggestedPresentationDelay: ${this._minSuggestedPresentationDelay}s`);
381
+ }
382
+
335
383
  // if video element is provided, add the listeres here. In this case ,there is no need to call attach.
336
384
  if (videoElement) {
337
385
  this._attach(videoElement);
338
386
  sdkLogger.warn("SenzaShakaPlayer constructor Adding videoElement in the constructor is going to be deprecated in the future. Please use attach method instead.");
339
387
  }
340
388
 
389
+ this.configure({
390
+ manifest: {
391
+ defaultPresentationDelay: this._minSuggestedPresentationDelay // in seconds
392
+ }
393
+ });
394
+
395
+ remotePlayer.configure({
396
+ minSuggestedPresentationDelay: this._minSuggestedPresentationDelay
397
+ });
398
+
399
+ this.addEventListener("buffering", () => {
400
+ if (this.videoElement) {
401
+ sdkLogger.info("Buffering at time:", this.videoElement.currentTime);
402
+ }
403
+ });
404
+
341
405
  }
342
406
 
407
+
343
408
  _attach(videoElement) {
344
409
  this.videoElement = videoElement;
345
410
 
@@ -618,16 +683,30 @@ export class SenzaShakaPlayer extends shaka.Player {
618
683
 
619
684
  // This callbakc will be activated when the manifest is loaded. It will trigger load of the remote player.
620
685
  // This will ensure that the remote player is loaded only after the manifest is loaded by local player.
621
- const responseFilterCallback = async (type) => {
622
- if (type === shaka.net.NetworkingEngine.RequestType.MANIFEST && !manifestLoadHandled) {
623
- manifestLoadHandled = true;
686
+ const responseFilterCallback = async (type, response) => {
687
+ if (type === shaka.net.NetworkingEngine.RequestType.MANIFEST) {
624
688
  try {
625
- await this._remotePlayerLoad(url, startTime);
626
- remoteLoadResolver();
689
+ if (response.data && this._minSuggestedPresentationDelay > 0) {
690
+ const manifestText = new TextDecoder().decode(response.data);
691
+ const modifiedText = this._updateManifestDelayIfBelowMinimum(manifestText);
692
+ if (modifiedText) {
693
+ const responseData = new TextEncoder().encode(modifiedText).buffer;
694
+ response.data = responseData;
695
+ }
696
+ }
627
697
  } catch (error) {
628
- remoteLoadRejecter(error);
698
+ sdkLogger.error("Error processing manifest:", error);
629
699
  }
630
700
 
701
+ if (!manifestLoadHandled) {
702
+ manifestLoadHandled = true;
703
+ try {
704
+ await this._remotePlayerLoad(url, startTime);
705
+ remoteLoadResolver();
706
+ } catch (error) {
707
+ remoteLoadRejecter(error);
708
+ }
709
+ }
631
710
  }
632
711
  };
633
712