@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.
- package/actionsExecutor/ActionExecutorContext.tsx +60 -84
- package/actionsExecutor/ScreenActions.ts +164 -0
- package/actionsExecutor/StorageActions.ts +110 -0
- package/actionsExecutor/feedDecorator.ts +171 -0
- package/actionsExecutor/screenResolver.ts +11 -0
- package/analyticsUtils/AnalyticPlayerListener.ts +5 -2
- package/analyticsUtils/AnalyticsEvents/helper.ts +1 -1
- package/analyticsUtils/AnalyticsEvents/sendHeaderClickEvent.ts +1 -1
- package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -1
- package/analyticsUtils/__tests__/analyticsUtils.test.js +0 -11
- package/analyticsUtils/index.tsx +3 -4
- package/analyticsUtils/manager.ts +1 -1
- package/analyticsUtils/playerAnalyticsTracker.ts +2 -1
- package/appUtils/accessibilityManager/const.ts +13 -0
- package/appUtils/accessibilityManager/hooks.ts +35 -1
- package/appUtils/accessibilityManager/index.ts +150 -29
- package/appUtils/accessibilityManager/utils.ts +24 -0
- package/appUtils/contextKeysManager/contextResolver.ts +42 -1
- package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +6 -2
- package/appUtils/focusManager/events.ts +2 -0
- package/appUtils/focusManager/index.ios.ts +27 -0
- package/appUtils/focusManager/index.ts +37 -34
- package/appUtils/focusManager/treeDataStructure/Tree/index.js +1 -1
- package/appUtils/focusManagerAux/utils/index.ts +94 -31
- package/appUtils/focusManagerAux/utils/utils.ios.ts +35 -0
- package/appUtils/platform/platformUtils.ts +33 -3
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
- package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
- package/appUtils/playerManager/conts.ts +21 -0
- package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
- package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
- package/arrayUtils/index.ts +5 -0
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +0 -1
- package/configurationUtils/index.ts +1 -1
- package/focusManager/FocusManager.ts +78 -4
- package/focusManager/aux/index.ts +98 -0
- package/focusManager/utils.ts +12 -6
- package/index.d.ts +1 -10
- package/manifestUtils/defaultManifestConfigurations/player.js +188 -2
- package/manifestUtils/index.js +4 -0
- package/manifestUtils/keys.js +33 -0
- package/manifestUtils/sharedConfiguration/screenPicker/stylesFields.js +6 -0
- package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
- package/navigationUtils/__tests__/mapContentTypesToRivers.test.ts +130 -0
- package/navigationUtils/index.ts +26 -21
- package/package.json +2 -3
- package/playerUtils/PlayerTTS/PlayerTTS.ts +359 -0
- package/playerUtils/PlayerTTS/index.ts +1 -0
- package/playerUtils/getPlayerActionButtons.ts +1 -1
- package/playerUtils/usePlayerTTS.ts +21 -0
- package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +15 -14
- package/reactHooks/cell-click/__tests__/index.test.js +3 -0
- package/reactHooks/cell-click/index.ts +8 -1
- package/reactHooks/debugging/__tests__/index.test.js +0 -1
- package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +47 -90
- package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +71 -31
- package/reactHooks/feed/index.ts +2 -0
- package/reactHooks/feed/useBatchLoading.ts +15 -8
- package/reactHooks/feed/useFeedLoader.tsx +36 -43
- package/reactHooks/feed/useInflatedUrl.ts +23 -29
- package/reactHooks/feed/useLoadPipesDataDispatch.ts +63 -0
- package/reactHooks/feed/usePipesCacheReset.ts +2 -2
- package/reactHooks/flatList/useSequentialRenderItem.tsx +3 -3
- package/reactHooks/layout/__tests__/index.test.tsx +3 -1
- package/reactHooks/layout/index.ts +1 -1
- package/reactHooks/layout/useDimensions/__tests__/useDimensions.test.ts +34 -36
- package/reactHooks/layout/useDimensions/useDimensions.ts +2 -3
- package/reactHooks/layout/useLayoutVersion.ts +5 -5
- package/reactHooks/navigation/index.ts +5 -7
- package/reactHooks/navigation/useRoute.ts +7 -2
- package/reactHooks/navigation/useScreenStateStore.ts +8 -0
- package/reactHooks/resolvers/__tests__/useCellResolver.test.tsx +4 -0
- package/reactHooks/state/index.ts +1 -1
- package/reactHooks/state/useHomeRiver.ts +4 -2
- package/reactHooks/state/useRivers.ts +7 -8
- package/screenPickerUtils/index.ts +13 -0
- package/storage/ScreenSingleValueProvider.ts +204 -0
- package/storage/ScreenStateMultiSelectProvider.ts +293 -0
- package/storage/StorageMultiSelectProvider.ts +192 -0
- package/storage/StorageSingleSelectProvider.ts +108 -0
- package/testUtils/index.tsx +7 -8
- package/time/BackgroundTimer.ts +1 -1
- package/utils/__tests__/endsWith.test.ts +30 -0
- package/utils/__tests__/find.test.ts +36 -0
- package/utils/__tests__/mapAccum.test.ts +73 -0
- package/utils/__tests__/omit.test.ts +19 -0
- package/utils/__tests__/path.test.ts +33 -0
- package/utils/__tests__/pathOr.test.ts +37 -0
- package/utils/__tests__/startsWith.test.ts +30 -0
- package/utils/__tests__/take.test.ts +40 -0
- package/utils/endsWith.ts +9 -0
- package/utils/find.ts +3 -0
- package/utils/index.ts +23 -1
- package/utils/mapAccum.ts +23 -0
- package/utils/omit.ts +5 -0
- package/utils/path.ts +5 -0
- package/utils/pathOr.ts +5 -0
- package/utils/startsWith.ts +9 -0
- 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
|
-
|
|
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
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
327
|
+
// Set focus with back button context to tabs-menu
|
|
328
|
+
landFocusToWithoutScrolling(selectedTabId);
|
|
329
|
+
}
|
|
337
330
|
|
|
338
|
-
|
|
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(
|
|
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
|
-
|
|
650
|
+
isFocusOn,
|
|
651
|
+
focusOnSelectedTopMenuItem,
|
|
652
|
+
focusOnSelectedTab,
|
|
653
|
+
isTabsScreenContentFocused,
|
|
651
654
|
};
|
|
652
655
|
})();
|
|
@@ -1,15 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
|
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
|
|
106
|
-
const
|
|
120
|
+
export const findSelectedTabId = (item: ZappEntry): string => {
|
|
121
|
+
const selectedTabId = getFocusableId(item.id);
|
|
107
122
|
|
|
108
|
-
return
|
|
109
|
-
}
|
|
123
|
+
return selectedTabId;
|
|
124
|
+
};
|
|
110
125
|
|
|
111
|
-
export const
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
export const isTabsScreenOnContentFocused = (node) => {
|
|
127
|
+
if (isRoot(node)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
114
130
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
131
|
+
if (isTopMenu(node)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
118
134
|
|
|
119
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)?.id;
|
|
155
|
+
if (isContent(node)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
130
158
|
|
|
131
|
-
return
|
|
159
|
+
return isCurrentFocusOnMenu(node.parent);
|
|
132
160
|
};
|
|
133
161
|
|
|
134
|
-
export const
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
|
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:
|
|
71
|
-
summary:
|
|
89
|
+
title: this.entry?.title || "",
|
|
90
|
+
summary: this.entry?.summary || "",
|
|
72
91
|
});
|
|
73
92
|
|
|
74
|
-
this.
|
|
75
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(
|
|
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
|
-
`
|
|
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
|
-
`
|
|
123
|
+
`loadFeedEntry: Feed was successfully loaded for url: ${feedUrl}`,
|
|
120
124
|
responseHelper.getLogsData()
|
|
121
125
|
);
|
|
122
126
|
|
|
123
|
-
const
|
|
127
|
+
const entry = responseHelper.responseData?.entry[0];
|
|
124
128
|
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
135
|
+
throw new Error(error);
|
|
136
|
+
}
|
|
137
137
|
|
|
138
|
-
return
|
|
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[]
|