@applicaster/zapp-react-native-utils 14.0.0-alpha.1118824347 → 14.0.0-alpha.1152359078

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.
Files changed (99) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +60 -84
  2. package/actionsExecutor/ScreenActions.ts +164 -0
  3. package/actionsExecutor/StorageActions.ts +110 -0
  4. package/actionsExecutor/feedDecorator.ts +171 -0
  5. package/actionsExecutor/screenResolver.ts +11 -0
  6. package/analyticsUtils/AnalyticPlayerListener.ts +5 -2
  7. package/analyticsUtils/AnalyticsEvents/helper.ts +1 -1
  8. package/analyticsUtils/AnalyticsEvents/sendHeaderClickEvent.ts +1 -1
  9. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -1
  10. package/analyticsUtils/__tests__/analyticsUtils.test.js +0 -11
  11. package/analyticsUtils/index.tsx +3 -4
  12. package/analyticsUtils/manager.ts +1 -1
  13. package/analyticsUtils/playerAnalyticsTracker.ts +2 -1
  14. package/appUtils/accessibilityManager/const.ts +13 -0
  15. package/appUtils/accessibilityManager/hooks.ts +35 -1
  16. package/appUtils/accessibilityManager/index.ts +150 -29
  17. package/appUtils/accessibilityManager/utils.ts +24 -0
  18. package/appUtils/contextKeysManager/contextResolver.ts +42 -1
  19. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +6 -2
  20. package/appUtils/focusManager/events.ts +2 -0
  21. package/appUtils/focusManager/index.ios.ts +27 -0
  22. package/appUtils/focusManager/index.ts +37 -34
  23. package/appUtils/focusManager/treeDataStructure/Tree/index.js +1 -1
  24. package/appUtils/focusManagerAux/utils/index.ts +94 -31
  25. package/appUtils/focusManagerAux/utils/utils.ios.ts +35 -0
  26. package/appUtils/platform/platformUtils.ts +33 -3
  27. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
  28. package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
  29. package/appUtils/playerManager/conts.ts +21 -0
  30. package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
  31. package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
  32. package/arrayUtils/index.ts +5 -0
  33. package/configurationUtils/__tests__/manifestKeyParser.test.ts +0 -1
  34. package/configurationUtils/index.ts +1 -1
  35. package/focusManager/FocusManager.ts +78 -4
  36. package/focusManager/aux/index.ts +98 -0
  37. package/focusManager/utils.ts +12 -6
  38. package/index.d.ts +1 -10
  39. package/manifestUtils/defaultManifestConfigurations/player.js +188 -2
  40. package/manifestUtils/index.js +4 -0
  41. package/manifestUtils/keys.js +33 -0
  42. package/manifestUtils/sharedConfiguration/screenPicker/stylesFields.js +6 -0
  43. package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
  44. package/navigationUtils/__tests__/mapContentTypesToRivers.test.ts +130 -0
  45. package/navigationUtils/index.ts +26 -21
  46. package/package.json +2 -3
  47. package/playerUtils/PlayerTTS/PlayerTTS.ts +359 -0
  48. package/playerUtils/PlayerTTS/index.ts +1 -0
  49. package/playerUtils/getPlayerActionButtons.ts +1 -1
  50. package/playerUtils/usePlayerTTS.ts +21 -0
  51. package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +15 -14
  52. package/reactHooks/cell-click/__tests__/index.test.js +3 -0
  53. package/reactHooks/cell-click/index.ts +8 -1
  54. package/reactHooks/debugging/__tests__/index.test.js +0 -1
  55. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +47 -90
  56. package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +71 -31
  57. package/reactHooks/feed/index.ts +2 -0
  58. package/reactHooks/feed/useBatchLoading.ts +15 -8
  59. package/reactHooks/feed/useFeedLoader.tsx +36 -43
  60. package/reactHooks/feed/useInflatedUrl.ts +23 -29
  61. package/reactHooks/feed/useLoadPipesDataDispatch.ts +63 -0
  62. package/reactHooks/feed/usePipesCacheReset.ts +2 -2
  63. package/reactHooks/flatList/useSequentialRenderItem.tsx +3 -3
  64. package/reactHooks/layout/__tests__/index.test.tsx +3 -1
  65. package/reactHooks/layout/index.ts +1 -1
  66. package/reactHooks/layout/useDimensions/__tests__/useDimensions.test.ts +34 -36
  67. package/reactHooks/layout/useDimensions/useDimensions.ts +2 -3
  68. package/reactHooks/layout/useLayoutVersion.ts +5 -5
  69. package/reactHooks/navigation/index.ts +5 -7
  70. package/reactHooks/navigation/useRoute.ts +7 -2
  71. package/reactHooks/navigation/useScreenStateStore.ts +8 -0
  72. package/reactHooks/resolvers/__tests__/useCellResolver.test.tsx +4 -0
  73. package/reactHooks/state/index.ts +1 -1
  74. package/reactHooks/state/useHomeRiver.ts +4 -2
  75. package/reactHooks/state/useRivers.ts +7 -8
  76. package/screenPickerUtils/index.ts +13 -0
  77. package/storage/ScreenSingleValueProvider.ts +204 -0
  78. package/storage/ScreenStateMultiSelectProvider.ts +293 -0
  79. package/storage/StorageMultiSelectProvider.ts +192 -0
  80. package/storage/StorageSingleSelectProvider.ts +108 -0
  81. package/testUtils/index.tsx +7 -8
  82. package/time/BackgroundTimer.ts +1 -1
  83. package/utils/__tests__/endsWith.test.ts +30 -0
  84. package/utils/__tests__/find.test.ts +36 -0
  85. package/utils/__tests__/mapAccum.test.ts +73 -0
  86. package/utils/__tests__/omit.test.ts +19 -0
  87. package/utils/__tests__/path.test.ts +33 -0
  88. package/utils/__tests__/pathOr.test.ts +37 -0
  89. package/utils/__tests__/startsWith.test.ts +30 -0
  90. package/utils/__tests__/take.test.ts +40 -0
  91. package/utils/endsWith.ts +9 -0
  92. package/utils/find.ts +3 -0
  93. package/utils/index.ts +23 -1
  94. package/utils/mapAccum.ts +23 -0
  95. package/utils/omit.ts +5 -0
  96. package/utils/path.ts +5 -0
  97. package/utils/pathOr.ts +5 -0
  98. package/utils/startsWith.ts +9 -0
  99. package/utils/take.ts +5 -0
@@ -3,6 +3,7 @@ import {
3
3
  isNilOrEmpty,
4
4
  isNotNil,
5
5
  } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
6
+ import { getFocusableId } from "@applicaster/zapp-react-native-utils/screenPickerUtils";
6
7
 
7
8
  import { Tree } from "./treeDataStructure/Tree";
8
9
  import {
@@ -15,12 +16,10 @@ import { coreLogger } from "../../logger";
15
16
  import { ACTION } from "./utils/enums";
16
17
 
17
18
  import {
18
- isTabsScreen,
19
- findSelectedTabId,
20
- findSelectedMenuId,
21
- isTabsMenuFocused,
19
+ isTabsScreenOnContentFocused,
22
20
  isCurrentFocusOnContent,
23
21
  isCurrentFocusOnMenu,
22
+ isCurrentFocusOn,
24
23
  } from "../focusManagerAux/utils";
25
24
 
26
25
  const logger = coreLogger.addSubsystem("focusManager");
@@ -300,7 +299,7 @@ export const focusManager = (function () {
300
299
  return isCurrentFocusOnMenu(currentFocusNode);
301
300
  }
302
301
 
303
- function landFocusTo(id) {
302
+ function landFocusToWithoutScrolling(id) {
304
303
  if (id) {
305
304
  // set focus on selected menu item
306
305
  const direction = undefined;
@@ -310,38 +309,39 @@ export const focusManager = (function () {
310
309
  preserveScroll: true,
311
310
  };
312
311
 
312
+ logger.log({ message: "landFocusTo", data: { id } });
313
+
313
314
  blur(direction);
314
315
  setFocus(id, direction, context);
315
316
  }
316
317
  }
317
318
 
318
- // Move focus to appropriate top navigation tab with context
319
- function focusTopNavigation() {
320
- // Store current focus for restoration
321
- // this.storeFocusState();
322
-
323
- if (isTabsScreen(focusableTree) && !isTabsMenuFocused(currentFocusNode)) {
324
- const selectedTabId = findSelectedTabId(focusableTree);
325
-
326
- console.log("debug_2", "FM - moveFocusToSelectedTab", { selectedTabId });
327
-
328
- landFocusTo(selectedTabId);
329
-
330
- return;
331
- }
319
+ function isTabsScreenContentFocused() {
320
+ return isTabsScreenOnContentFocused(currentFocusNode);
321
+ }
332
322
 
333
- // Set focus with back button context
334
- const selectedMenuItemId = findSelectedMenuId(focusableTree);
323
+ function focusOnSelectedTab(item: ZappEntry): void {
324
+ // Move focus to appropriate top navigation tab with context
325
+ const selectedTabId = getFocusableId(item.id);
335
326
 
336
- console.log("debug_2", "IM - moveFocusToTopMenu", { selectedMenuItemId });
327
+ // Set focus with back button context to tabs-menu
328
+ landFocusToWithoutScrolling(selectedTabId);
329
+ }
337
330
 
338
- landFocusTo(selectedMenuItemId);
331
+ function focusOnSelectedTopMenuItem(
332
+ selectedMenuItemId: Option<string>
333
+ ): void {
334
+ // Set focus with back button context to top-menu
335
+ landFocusToWithoutScrolling(selectedMenuItemId);
339
336
  }
340
337
 
341
338
  /**
342
339
  * sets the initial focus when the screen loads, or when focus is lost
343
340
  */
344
- function setInitialFocus(lastAddedParentNode?: any) {
341
+ function setInitialFocus(
342
+ lastAddedParentNode?: any,
343
+ context?: FocusManager.FocusContext
344
+ ) {
345
345
  const preferredFocus = findPriorityItem(
346
346
  lastAddedParentNode?.children || focusableTree.root.children
347
347
  );
@@ -387,7 +387,7 @@ export const focusManager = (function () {
387
387
  },
388
388
  });
389
389
 
390
- focusableItem && setFocus(focusCandidate.id, null);
390
+ focusableItem && setFocus(focusCandidate.id, null, context);
391
391
 
392
392
  return { success: true };
393
393
  }
@@ -544,12 +544,6 @@ export const focusManager = (function () {
544
544
  return haveSameParentBeforeRoot(currentFocusNode, R.last(routes));
545
545
  }
546
546
 
547
- function isOnRootScreen() {
548
- const routes = R.pathOr([], ["root", "children"], focusableTree);
549
-
550
- return routes.length <= 1;
551
- }
552
-
553
547
  function recoverFocus() {
554
548
  if (!isCurrentFocusOnTheTopScreen()) {
555
549
  // We've failed to set focused node on the new screen => run focus recovery
@@ -613,6 +607,14 @@ export const focusManager = (function () {
613
607
  return preferredFocus[0];
614
608
  }
615
609
 
610
+ function isFocusOn(id): boolean {
611
+ return (
612
+ id &&
613
+ isCurrentFocusOnTheTopScreen() &&
614
+ isCurrentFocusOn(id, currentFocusNode)
615
+ );
616
+ }
617
+
616
618
  /**
617
619
  * this is the list of the functions available externally
618
620
  * when importing the focus manager
@@ -643,10 +645,11 @@ export const focusManager = (function () {
643
645
  recoverFocus,
644
646
  isCurrentFocusOnTheTopScreen,
645
647
  findPreferredFocusChild,
646
-
647
- focusTopNavigation,
648
648
  isFocusOnContent,
649
649
  isFocusOnMenu,
650
- isOnRootScreen,
650
+ isFocusOn,
651
+ focusOnSelectedTopMenuItem,
652
+ focusOnSelectedTab,
653
+ isTabsScreenContentFocused,
651
654
  };
652
655
  })();
@@ -142,7 +142,7 @@ export class Tree {
142
142
  this.hasGroupID(node)
143
143
  ? "Make sure that there are no id duplicates inside the " +
144
144
  existingNode.parent.id +
145
- " group."
145
+ " group. This can as well happen when the component is re-mounted"
146
146
  : ""
147
147
  }`,
148
148
  });
@@ -1,15 +1,30 @@
1
- import { isNil } from "@applicaster/zapp-react-native-utils/utils";
2
- import { find, last, pathOr, startsWith } from "ramda";
1
+ import {
2
+ isNil,
3
+ last,
4
+ startsWith,
5
+ find,
6
+ pathOr,
7
+ } from "@applicaster/zapp-react-native-utils/utils";
8
+
3
9
  import {
4
10
  QUICK_BRICK_CONTENT,
5
11
  QUICK_BRICK_NAVBAR,
6
12
  } from "@applicaster/quick-brick-core/const";
7
13
 
14
+ import {
15
+ getFocusableId,
16
+ SCREEN_PICKER_CONTAINER,
17
+ } from "@applicaster/zapp-react-native-utils/screenPickerUtils";
18
+
8
19
  // run check each 300 ms
9
20
  // run this check too often could lead to performance penalty on low-end devices
10
21
  const HOW_OFTEN_TO_CHECK_CONDITION = 300; // ms
11
22
 
12
- const TABS_GROUP_ID = "PickerSelector.sp-river";
23
+ const isTopMenu = (node) => startsWith(QUICK_BRICK_NAVBAR, node?.id);
24
+ const isContent = (node) => startsWith(QUICK_BRICK_CONTENT, node?.id);
25
+ const isRoot = (node) => node?.id === "root";
26
+
27
+ const isScrenPicker = (node) => startsWith(SCREEN_PICKER_CONTAINER, node?.id);
13
28
 
14
29
  type Props = {
15
30
  maxTimeout: number;
@@ -102,46 +117,94 @@ export const waitForContent = (focusableTree) => {
102
117
  });
103
118
  };
104
119
 
105
- export function isTabsScreen(focusableTree) {
106
- const tabsGroup = focusableTree.findInTree(TABS_GROUP_ID);
120
+ export const findSelectedTabId = (item: ZappEntry): string => {
121
+ const selectedTabId = getFocusableId(item.id);
107
122
 
108
- return !!tabsGroup;
109
- }
123
+ return selectedTabId;
124
+ };
110
125
 
111
- export const findSelectedTabId = (focusableTree) => {
112
- // FIXME - find elegant way how to get ID of selected tab
113
- const tabsGroup = focusableTree.findInTree(TABS_GROUP_ID);
126
+ export const isTabsScreenOnContentFocused = (node) => {
127
+ if (isRoot(node)) {
128
+ return false;
129
+ }
114
130
 
115
- const selectedTabId = tabsGroup.children.find(
116
- (child) => child.component.props.preferredFocus // ?? selected
117
- )?.id;
131
+ if (isTopMenu(node)) {
132
+ return false;
133
+ }
118
134
 
119
- return selectedTabId;
135
+ if (isContent(node)) {
136
+ return false;
137
+ }
138
+
139
+ if (isScrenPicker(node)) {
140
+ return true;
141
+ }
142
+
143
+ return isTabsScreenOnContentFocused(node.parent);
120
144
  };
121
145
 
122
- export const findSelectedMenuId = (focusableTree) => {
123
- // Set focus with back button context
124
- const navbar = getNavbarNode(focusableTree);
146
+ export const isCurrentFocusOnMenu = (node) => {
147
+ if (isRoot(node)) {
148
+ return false;
149
+ }
150
+
151
+ if (isTopMenu(node)) {
152
+ return true;
153
+ }
125
154
 
126
- const selectedMenuItemId = find(
127
- (child) => child.component.props.selected,
128
- navbar.children
129
- )?.id;
155
+ if (isContent(node)) {
156
+ return false;
157
+ }
130
158
 
131
- return selectedMenuItemId;
159
+ return isCurrentFocusOnMenu(node.parent);
132
160
  };
133
161
 
134
- export const isTabsMenuFocused = (node) => {
135
- // FIXME - find elegant way how to get ID of selected tab
136
- return node.parent.id === TABS_GROUP_ID;
162
+ export const isCurrentFocusOnContent = (node) => {
163
+ if (isRoot(node)) {
164
+ return false;
165
+ }
166
+
167
+ if (isTopMenu(node)) {
168
+ return false;
169
+ }
170
+
171
+ if (isContent(node)) {
172
+ return true;
173
+ }
174
+
175
+ return isCurrentFocusOnContent(node.parent);
137
176
  };
138
177
 
139
- export const isCurrentFocusOnMenu = (node) => {
140
- // FIXME
141
- return node.parent.id.startsWith(QUICK_BRICK_NAVBAR);
178
+ export const isCurrentFocusOn = (id, node) => {
179
+ if (!node) {
180
+ return false;
181
+ }
182
+
183
+ if (isRoot(node)) {
184
+ return false;
185
+ }
186
+
187
+ if (node?.id === id) {
188
+ return true;
189
+ }
190
+
191
+ return isCurrentFocusOn(id, node.parent);
142
192
  };
143
193
 
144
- export const isCurrentFocusOnContent = (node) => {
145
- // FIXME
146
- return !isCurrentFocusOnMenu(node);
194
+ export const isChildOf = (focusableTree, childId, parentId) => {
195
+ if (isNil(childId) || isNil(parentId)) {
196
+ return false;
197
+ }
198
+
199
+ const childNode = focusableTree.findInTree(childId);
200
+
201
+ if (isNil(childNode)) {
202
+ return false;
203
+ }
204
+
205
+ if (childNode.parent?.id === parentId) {
206
+ return true;
207
+ }
208
+
209
+ return isChildOf(focusableTree, childNode.parent?.id, parentId);
147
210
  };
@@ -0,0 +1,35 @@
1
+ import { ReplaySubject } from "rxjs";
2
+ import { filter } from "rxjs/operators";
3
+ import { BUTTON_PREFIX } from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/tv/TvActionButtons/const";
4
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
5
+
6
+ type FocusableID = string;
7
+ type RegistrationEvent = {
8
+ id: FocusableID;
9
+ registered: boolean;
10
+ };
11
+
12
+ const isFocusableButton = (id: Option<FocusableID>): boolean =>
13
+ id && id.includes?.(BUTTON_PREFIX);
14
+
15
+ const registeredSubject$ = new ReplaySubject<RegistrationEvent>(1);
16
+
17
+ export const focusableButtonsRegistration$ = (focusableGroupId: string) =>
18
+ registeredSubject$.pipe(
19
+ filter(
20
+ (value) =>
21
+ value.registered && focusManager.isChildOf(value.id, focusableGroupId)
22
+ )
23
+ );
24
+
25
+ export const emitRegistered = (id: Option<FocusableID>): void => {
26
+ if (isFocusableButton(id)) {
27
+ registeredSubject$.next({ id, registered: true });
28
+ }
29
+ };
30
+
31
+ export const emitUnregistered = (id: Option<FocusableID>): void => {
32
+ if (isFocusableButton(id)) {
33
+ registeredSubject$.next({ id, registered: false });
34
+ }
35
+ };
@@ -10,6 +10,7 @@ import { PLATFORM_KEYS, PLATFORMS, ZappPlatform } from "./const";
10
10
  import { createLogger, utilsLogger } from "../../logger";
11
11
  import { getPlatform } from "../../reactUtils";
12
12
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
13
+ import { calculateReadingTime } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/utils";
13
14
 
14
15
  const { log_debug } = createLogger({
15
16
  category: "General",
@@ -212,36 +213,65 @@ export class TTSManager {
212
213
  }
213
214
 
214
215
  readText(text: string) {
216
+ this.ttsState$.next(true);
217
+
215
218
  if (isSamsungPlatform() && window.speechSynthesis) {
216
219
  const utterance = new SpeechSynthesisUtterance(text);
220
+
221
+ window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text
217
222
  window.speechSynthesis.speak(utterance);
223
+
224
+ // Estimate reading time and set inactive when done
225
+ this.scheduleTTSComplete(text);
218
226
  }
219
227
 
220
228
  if (isLgPlatform() && window.webOS?.service) {
221
229
  try {
222
230
  window.webOS.service.request("luna://com.webos.service.tts", {
223
231
  method: "speak",
224
- onFailure(error: any) {
232
+ onFailure: (error: any) => {
225
233
  log_debug("There was a failure setting up webOS TTS service", {
226
234
  error,
227
235
  });
236
+
237
+ this.ttsState$.next(false);
228
238
  },
229
- onSuccess(response: any) {
239
+ onSuccess: (response: any) => {
230
240
  log_debug("webOS TTS service is configured successfully", {
231
241
  response,
232
242
  });
243
+
244
+ // Estimate reading time and set inactive when done
245
+ this.scheduleTTSComplete(text);
233
246
  },
234
247
  parameters: {
235
248
  text,
249
+ clear: true, // Clear any previous speech before speaking new text
236
250
  },
237
251
  });
238
252
  } catch (error) {
239
253
  log_debug("webOS TTS service error", { error });
254
+ this.ttsState$.next(false);
240
255
  }
241
256
  }
242
257
 
243
- if (!window.VIZIO?.Chromevox) return;
258
+ if (!window.VIZIO?.Chromevox) {
259
+ // For platforms without TTS, estimate reading time
260
+ this.scheduleTTSComplete(text);
261
+
262
+ return;
263
+ }
244
264
 
245
265
  window.VIZIO.Chromevox.play(text);
266
+ // Estimate reading time and set inactive when done
267
+ this.scheduleTTSComplete(text);
268
+ }
269
+
270
+ private scheduleTTSComplete(text: string) {
271
+ const readingTime = calculateReadingTime(text);
272
+
273
+ setTimeout(() => {
274
+ this.ttsState$.next(false);
275
+ }, readingTime);
246
276
  }
247
277
  }
@@ -5,6 +5,7 @@ import { createLogger, utilsLogger } from "../../../logger";
5
5
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
6
6
  import {
7
7
  findPluginByIdentifier,
8
+ loadFeedEntry,
8
9
  loadFeedAndPrefetchThumbnailImage,
9
10
  parseTimeToSeconds,
10
11
  retrieveFeedUrl,
@@ -26,6 +27,19 @@ type ChapterMarkerOriginal = {
26
27
  actions: ActionChapter[];
27
28
  };
28
29
 
30
+ export type LiveMetadataEvent = {
31
+ programId: string;
32
+ assetId: string;
33
+ title: string;
34
+ programStartTime: string;
35
+ programEndTime: string;
36
+ };
37
+
38
+ export type LiveMetadataConfig = {
39
+ updateUrl: string;
40
+ updateInterval: number;
41
+ };
42
+
29
43
  export type TitleSummaryEvent = {
30
44
  title: string | number;
31
45
  summary: string | number;
@@ -50,11 +64,13 @@ export type PlayNextState = PlayNextConfig & {
50
64
  export class OverlaysObserver {
51
65
  readonly chapterSubject: BehaviorSubject<ChapterMarkerEvent>;
52
66
  private playNextSubject: BehaviorSubject<PlayNextState>;
67
+ private liveMetadataSubject: BehaviorSubject<LiveMetadataEvent>;
53
68
  private titleSummarySubject: BehaviorSubject<TitleSummaryEvent>;
54
69
  private feedUrl: string;
55
70
  private reloadData: () => void;
56
71
  private updateTitleAndDescription: (data: any) => void;
57
72
  private feedDataInterval: any;
73
+ private liveMetadataUpdateInterval: any;
58
74
  private releasePlayerObserver?: () => void;
59
75
  readonly entry: ZappEntry;
60
76
  private chapterMarkerEvents: ChapterMarkerEvent[];
@@ -66,13 +82,17 @@ export class OverlaysObserver {
66
82
  this.chapterSubject = new BehaviorSubject(null);
67
83
  this.playNextSubject = new BehaviorSubject(null);
68
84
 
85
+ this.player = player;
86
+ this.entry = player.getEntry();
87
+
69
88
  this.titleSummarySubject = new BehaviorSubject<TitleSummaryEvent>({
70
- title: player.getEntry()?.title || "",
71
- summary: player.getEntry()?.summary || "",
89
+ title: this.entry?.title || "",
90
+ summary: this.entry?.summary || "",
72
91
  });
73
92
 
74
- this.entry = player.getEntry();
75
- this.player = player;
93
+ this.liveMetadataSubject = new BehaviorSubject<LiveMetadataEvent>(
94
+ this.entry?.extensions?.liveMetadata || null
95
+ );
76
96
 
77
97
  this.chapterMarkerEvents = this.prepareChapterMarkers();
78
98
  this.releasePlayerObserver = this.subscribeToPlayerEvents();
@@ -80,7 +100,9 @@ export class OverlaysObserver {
80
100
  this.reloadData = () => {};
81
101
  this.updateTitleAndDescription = () => {};
82
102
  this.feedDataInterval = null;
103
+ this.liveMetadataUpdateInterval = null;
83
104
  void this.preparePlayNext();
105
+ void this.prepareLiveMetadata();
84
106
  }
85
107
 
86
108
  private setupFeedDataInterval(interval: number) {
@@ -98,6 +120,13 @@ export class OverlaysObserver {
98
120
  }
99
121
  }
100
122
 
123
+ public clearLiveMetadataUpdateInterval() {
124
+ if (this.liveMetadataUpdateInterval) {
125
+ clearInterval(this.liveMetadataUpdateInterval);
126
+ this.liveMetadataUpdateInterval = null;
127
+ }
128
+ }
129
+
101
130
  public setFeedDataHandlers(
102
131
  feedUrl: string,
103
132
  reloadData: () => void,
@@ -197,6 +226,48 @@ export class OverlaysObserver {
197
226
  }
198
227
  };
199
228
 
229
+ prepareLiveMetadata = async () => {
230
+ if (!this.player?.isLive?.()) {
231
+ log_debug("prepareLiveMetadata: Player is not live. Skipping...");
232
+
233
+ return;
234
+ }
235
+
236
+ const config: LiveMetadataConfig =
237
+ this.entry?.extensions?.liveMetadataConfig;
238
+
239
+ if (!config?.updateUrl || !config?.updateInterval) {
240
+ log_debug(
241
+ "prepareLiveMetadata: Live metadata configuration is not available. Skipping...",
242
+ { config }
243
+ );
244
+
245
+ return;
246
+ }
247
+
248
+ const reloadData = async () => {
249
+ try {
250
+ const entry = await loadFeedEntry(config.updateUrl, this.entry);
251
+
252
+ this.onLiveMetadataUpdated(entry?.extensions?.liveMetadata || null);
253
+ } catch (error) {
254
+ log_error("prepareLiveMetadata: Metadata fetching failed", {
255
+ error,
256
+ config,
257
+ });
258
+ }
259
+ };
260
+
261
+ log_debug("prepareLiveMetadata: Setting up live metadata observer update", {
262
+ config,
263
+ });
264
+
265
+ const interval = Number(config?.updateInterval) || 60;
266
+
267
+ this.clearLiveMetadataUpdateInterval();
268
+ this.liveMetadataUpdateInterval = setInterval(reloadData, interval * 1000);
269
+ };
270
+
200
271
  // TODO: Hack for video end, will be replaced with playlist prev/next in the future
201
272
  getPlayNextEntry = () =>
202
273
  !this.isCanceledByUser ? this.playNextConfig?.entry : null;
@@ -317,9 +388,11 @@ export class OverlaysObserver {
317
388
  onPlayerClose = () => {
318
389
  this.chapterSubject.complete();
319
390
  this.playNextSubject.complete();
391
+ this.liveMetadataSubject.complete();
320
392
  this.titleSummarySubject.complete();
321
393
  this.releasePlayerObserver?.();
322
394
  this.clearFeedDataInterval();
395
+ this.clearLiveMetadataUpdateInterval();
323
396
  this.releasePlayerObserver = null;
324
397
  };
325
398
 
@@ -352,4 +425,21 @@ export class OverlaysObserver {
352
425
  (prev, curr) => prev?.triggerTime === curr?.triggerTime
353
426
  )
354
427
  );
428
+
429
+ private onLiveMetadataUpdated = (liveMetadataEvent: LiveMetadataEvent) => {
430
+ this.liveMetadataSubject.next(liveMetadataEvent);
431
+ };
432
+
433
+ public getLiveMetadataObservable = (): Observable<LiveMetadataEvent> => {
434
+ return this.liveMetadataSubject.pipe(
435
+ distinctUntilChanged(
436
+ (prev, curr) =>
437
+ prev?.programId === curr?.programId && prev?.assetId === curr?.assetId
438
+ )
439
+ );
440
+ };
441
+
442
+ public getLiveMetadataValue = (): LiveMetadataEvent => {
443
+ return this.liveMetadataSubject.value;
444
+ };
355
445
  }
@@ -92,22 +92,26 @@ export const prefetchImage = (playableItem: ZappEntry, config: any = {}) => {
92
92
  }
93
93
  };
94
94
 
95
- export const loadFeedAndPrefetchThumbnailImage = async (
96
- playNextFeedUrl: string,
97
- entry: ZappEntry,
98
- playNextPlugin
99
- ) => {
95
+ /**
96
+ * Loads a feed entry from the given feed URL using the provided Zapp entry as context.
97
+ *
98
+ * @param {string} feedUrl - The URL of the feed to load the entry from.
99
+ * @param {ZappEntry} entry - The Zapp entry to use as context for the request.
100
+ * @returns {Promise<ZappEntry>} A promise that resolves to the loaded Zapp entry.
101
+ * @throws {Error} If the feed loading fails or no entry is found in the response.
102
+ */
103
+ export const loadFeedEntry = async (feedUrl: string, entry: ZappEntry) => {
100
104
  const requestBuilder = new RequestBuilder()
101
105
  .setEntryContext(entry)
102
106
  .setScreenContext({} as ZappRiver)
103
- .setUrl(playNextFeedUrl);
107
+ .setUrl(feedUrl);
104
108
 
105
109
  const responseObject = await requestBuilder.call<ZappEntry>();
106
110
  const responseHelper = new PipesClientResponseHelper(responseObject);
107
111
 
108
112
  if (responseHelper.error) {
109
113
  log_error(
110
- `loadFeedAndPrefetchThumbnailImage: loading failed with error: ${responseHelper.error.message}. Play next observer, will not be executed`,
114
+ `loadFeedEntry: loading failed with error: ${responseHelper.error.message}. Observer will not be executed`,
111
115
  {
112
116
  response: responseHelper.getLogsData(),
113
117
  }
@@ -116,29 +120,37 @@ export const loadFeedAndPrefetchThumbnailImage = async (
116
120
  throw responseHelper.error;
117
121
  } else {
118
122
  log_info(
119
- `loadFeedAndPrefetchThumbnailImage: Play next url was successfully loaded for url: ${playNextFeedUrl}. Prefetching image`,
123
+ `loadFeedEntry: Feed was successfully loaded for url: ${feedUrl}`,
120
124
  responseHelper.getLogsData()
121
125
  );
122
126
 
123
- const playNextEntry = responseHelper.responseData?.entry[0];
127
+ const entry = responseHelper.responseData?.entry[0];
124
128
 
125
- if (!playNextEntry) {
126
- log_error(
127
- "loadFeedAndPrefetchThumbnailImage: Can not retrieve play next entry, feed was loaded but no entry was found",
128
- responseHelper.getLogsData()
129
- );
129
+ if (!entry) {
130
+ const error =
131
+ "loadFeedEntry: Can not retrieve entry, feed was loaded but no entry was found";
130
132
 
131
- throw new Error(
132
- "Can not retrieve play next entry, feed was loaded but no entry was found"
133
- );
134
- }
133
+ log_error(error, responseHelper.getLogsData());
135
134
 
136
- prefetchImage(playNextEntry, playNextPlugin?.configuration);
135
+ throw new Error(error);
136
+ }
137
137
 
138
- return playNextEntry;
138
+ return entry;
139
139
  }
140
140
  };
141
141
 
142
+ export const loadFeedAndPrefetchThumbnailImage = async (
143
+ playNextFeedUrl: string,
144
+ entry: ZappEntry,
145
+ playNextPlugin
146
+ ) => {
147
+ const playNextEntry = await loadFeedEntry(playNextFeedUrl, entry);
148
+
149
+ prefetchImage(playNextEntry, playNextPlugin?.configuration);
150
+
151
+ return playNextEntry;
152
+ };
153
+
142
154
  export const findPluginByIdentifier = (
143
155
  identifier: string,
144
156
  plugins: ZappPlugin[]