@applicaster/zapp-react-native-ui-components 15.0.0-alpha.4429053208 → 15.0.0-alpha.4435057569
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/Components/Cell/Cell.tsx +6 -0
- package/Components/Cell/CellWithFocusable.tsx +9 -0
- package/Components/Focusable/FocusableTvOS.tsx +2 -2
- package/Components/FocusableGroup/FocusableTvOS.tsx +4 -27
- package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
- package/Components/GeneralContentScreen/utils/useCurationAPI.ts +7 -7
- package/Components/HandlePlayable/utils.ts +31 -0
- package/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
- package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +112 -0
- package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +38 -16
- package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +40 -39
- package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/Image.test.tsx +95 -0
- package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +86 -0
- package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/index.test.ts +141 -0
- package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
- package/Components/MasterCell/DefaultComponents/SecondaryImage/index.ts +1 -1
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +6 -2
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +233 -11
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +19 -15
- package/Components/MasterCell/hoc/__tests__/withAsyncRender.test.tsx +219 -0
- package/Components/MasterCell/hoc/withAsyncRender.tsx +9 -7
- package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +17 -9
- package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +16 -8
- package/Components/OfflineHandler/NotificationView/utils.ts +34 -0
- package/Components/PlayerContainer/PlayerContainer.tsx +1 -2
- package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +39 -9
- package/Components/River/ComponentsMap/ComponentsMap.tsx +16 -0
- package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
- package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
- package/Components/River/__tests__/componentsMap.test.js +38 -0
- package/Components/Screen/orientationHandler.ts +7 -10
- package/Components/Tabs/TabContent.tsx +7 -4
- package/Components/VideoLive/LiveImageManager.ts +143 -53
- package/Components/VideoLive/PlayerLiveImageComponent.tsx +22 -26
- package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
- package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
- package/Components/Viewport/ViewportAware/index.tsx +16 -7
- package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
- package/Contexts/ScreenContext/index.tsx +25 -18
- package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
- package/Contexts/ZappHookModalContext/index.tsx +37 -61
- package/Contexts/index.ts +0 -2
- package/events/index.ts +3 -0
- package/events/scrollEndReached.ts +15 -0
- package/package.json +5 -5
- package/Components/FocusableGroup/hooks/__tests__/useIsFocusEnabled.test.ts +0 -113
- package/Components/FocusableGroup/hooks/index.ts +0 -1
- package/Components/FocusableGroup/hooks/useIsFocusEnabled.ts +0 -68
- package/Contexts/AboveTabsScreenContext/index.tsx +0 -33
package/Components/Cell/Cell.tsx
CHANGED
|
@@ -26,11 +26,15 @@ type Props = {
|
|
|
26
26
|
componentAnchorPointY: number;
|
|
27
27
|
headerOffset?: number;
|
|
28
28
|
extraAnchorPointYOffset?: number;
|
|
29
|
+
componentPaddingTop?: number;
|
|
29
30
|
}) => void;
|
|
30
31
|
offsetUpdater: (arg1: string, arg2: number, arg3: number) => number;
|
|
31
32
|
componentId: string;
|
|
32
33
|
component: {
|
|
33
34
|
id: string;
|
|
35
|
+
styles?: {
|
|
36
|
+
component_padding_top?: number;
|
|
37
|
+
};
|
|
34
38
|
};
|
|
35
39
|
selected?: boolean;
|
|
36
40
|
CellRenderer: React.FunctionComponent<any> & {
|
|
@@ -178,6 +182,8 @@ export class CellComponent extends React.Component<Props, State> {
|
|
|
178
182
|
componentAnchorPointY,
|
|
179
183
|
headerOffset,
|
|
180
184
|
extraAnchorPointYOffset,
|
|
185
|
+
componentPaddingTop:
|
|
186
|
+
this.props?.component?.styles?.component_padding_top,
|
|
181
187
|
});
|
|
182
188
|
}
|
|
183
189
|
}
|
|
@@ -2,6 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
|
|
3
3
|
import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
|
|
4
4
|
import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
|
|
5
|
+
import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
5
6
|
|
|
6
7
|
import { useCellState } from "../MasterCell/utils";
|
|
7
8
|
import { FocusableGroup } from "../FocusableGroup";
|
|
@@ -26,6 +27,13 @@ type Props = {
|
|
|
26
27
|
|
|
27
28
|
const addPrefix = (id: string) => `focusable-cell-wrapper-${id}`;
|
|
28
29
|
|
|
30
|
+
const wrapperStyles = {
|
|
31
|
+
flex: platformSelect({
|
|
32
|
+
tvos: 1,
|
|
33
|
+
default: undefined,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
|
|
29
37
|
export function CellWithFocusable(props: Props) {
|
|
30
38
|
const {
|
|
31
39
|
index,
|
|
@@ -94,6 +102,7 @@ export function CellWithFocusable(props: Props) {
|
|
|
94
102
|
onFocus={onGroupFocus}
|
|
95
103
|
onBlur={onGroupBlur}
|
|
96
104
|
skipFocusManagerRegistration={skipFocusManagerRegistration}
|
|
105
|
+
style={wrapperStyles}
|
|
97
106
|
>
|
|
98
107
|
<CellWrapper style={styles.cellWrapper}>
|
|
99
108
|
<CellRenderer
|
|
@@ -13,7 +13,7 @@ import { findNodeHandle, ViewStyle } from "react-native";
|
|
|
13
13
|
import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
emitFocused,
|
|
17
17
|
emitNativeRegistered,
|
|
18
18
|
} from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
|
|
19
19
|
|
|
@@ -91,7 +91,7 @@ export class Focusable extends BaseFocusable<Props> {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const id: string = nativeEvent.itemID;
|
|
94
|
-
|
|
94
|
+
emitFocused(id);
|
|
95
95
|
|
|
96
96
|
onFocus(nativeEvent);
|
|
97
97
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { compose } from "@applicaster/zapp-react-native-utils/utils";
|
|
3
2
|
import { FocusableGroupNative } from "@applicaster/zapp-react-native-ui-components/Components/NativeFocusables";
|
|
4
3
|
import { BaseFocusable } from "@applicaster/zapp-react-native-ui-components/Components/BaseFocusable";
|
|
5
4
|
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
@@ -7,9 +6,6 @@ import { LayoutContext } from "@applicaster/zapp-react-native-tvos-app/Context/L
|
|
|
7
6
|
import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute";
|
|
8
7
|
import { isScreenPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypes";
|
|
9
8
|
import { emitNativeRegistered } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
|
|
10
|
-
import { withAboveTabsScreenContextConsumer } from "@applicaster/zapp-react-native-ui-components/Contexts/AboveTabsScreenContext";
|
|
11
|
-
|
|
12
|
-
import { useIsFocusEnabled } from "./hooks";
|
|
13
9
|
|
|
14
10
|
const { log_verbose } = createLogger({
|
|
15
11
|
subsystem: "General",
|
|
@@ -91,8 +87,8 @@ class FocusableGroupComponent extends BaseFocusable<Props> {
|
|
|
91
87
|
}
|
|
92
88
|
}
|
|
93
89
|
|
|
94
|
-
export const
|
|
95
|
-
return function
|
|
90
|
+
export const withFocusDisabled = (Component) => {
|
|
91
|
+
return function WithFocusDisabled(props) {
|
|
96
92
|
// @ts-ignore
|
|
97
93
|
const { screenFocusBlocked } = React.useContext(LayoutContext.ReactContext);
|
|
98
94
|
|
|
@@ -102,27 +98,8 @@ export const withFocusDisabledHOC = (Component) => {
|
|
|
102
98
|
|
|
103
99
|
const blockScreenFocus = isPlayerPresented === false && screenFocusBlocked;
|
|
104
100
|
|
|
105
|
-
return
|
|
106
|
-
<Component
|
|
107
|
-
{...props}
|
|
108
|
-
isFocusDisabled={blockScreenFocus || props.isFocusDisabled}
|
|
109
|
-
/>
|
|
110
|
-
);
|
|
111
|
-
};
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const withAboveTabsScreenHOC = (Component) => {
|
|
115
|
-
return function WithAboveTabsScreenHOC(props) {
|
|
116
|
-
const { aboveTabsScreen } = props;
|
|
117
|
-
|
|
118
|
-
const isFocusEnabled = useIsFocusEnabled(aboveTabsScreen);
|
|
119
|
-
|
|
120
|
-
return <Component {...props} isFocusDisabled={!isFocusEnabled} />;
|
|
101
|
+
return <Component {...props} isFocusDisabled={blockScreenFocus} />;
|
|
121
102
|
};
|
|
122
103
|
};
|
|
123
104
|
|
|
124
|
-
export const FocusableGroup =
|
|
125
|
-
withAboveTabsScreenContextConsumer,
|
|
126
|
-
withAboveTabsScreenHOC,
|
|
127
|
-
withFocusDisabledHOC
|
|
128
|
-
)(FocusableGroupComponent);
|
|
105
|
+
export const FocusableGroup = withFocusDisabled(FocusableGroupComponent);
|
|
@@ -9,10 +9,9 @@ import {
|
|
|
9
9
|
import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils";
|
|
10
10
|
import { Categories } from "./logger";
|
|
11
11
|
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
12
|
-
import {
|
|
12
|
+
import { useScreenContext } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
-
ZappPipesEntryContext,
|
|
16
15
|
ZappPipesScreenContext,
|
|
17
16
|
ZappPipesSearchContext,
|
|
18
17
|
} from "@applicaster/zapp-react-native-ui-components/Contexts";
|
|
@@ -136,7 +135,6 @@ export const useCurationAPI = (
|
|
|
136
135
|
[components]
|
|
137
136
|
);
|
|
138
137
|
|
|
139
|
-
const { pathname } = useRoute();
|
|
140
138
|
const [searchContext] = ZappPipesSearchContext.useZappPipesContext();
|
|
141
139
|
const [screenContext] = ZappPipesScreenContext.useZappPipesContext();
|
|
142
140
|
|
|
@@ -146,10 +144,12 @@ export const useCurationAPI = (
|
|
|
146
144
|
screenContextType === TABS_SCREEN_TYPE ||
|
|
147
145
|
screenContextType === QB_TABS_SCREEN_TYPE;
|
|
148
146
|
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
const screenContextData = useScreenContext();
|
|
148
|
+
|
|
149
|
+
const entryContext = ((isNestedScreen && screenContextData?.nested?.entry
|
|
150
|
+
? screenContextData?.nested?.entry
|
|
151
|
+
: (screenContextData?.entry?.payload ?? screenContextData?.entry)) ||
|
|
152
|
+
{}) as ZappEntry;
|
|
153
153
|
|
|
154
154
|
const urlsMap = useMemo<{ [key: string]: string }>(() => {
|
|
155
155
|
const map = {};
|
|
@@ -5,6 +5,14 @@ import {
|
|
|
5
5
|
|
|
6
6
|
import { CHROMECAST_PLUGIN_ID, YOUTUBE_PLUGIN_ID } from "./const";
|
|
7
7
|
import { omit } from "@applicaster/zapp-react-native-utils/utils";
|
|
8
|
+
import { getXray } from "@applicaster/zapp-react-native-utils/logger";
|
|
9
|
+
|
|
10
|
+
const { Logger } = getXray();
|
|
11
|
+
|
|
12
|
+
const logger = new Logger(
|
|
13
|
+
"QuickBrick",
|
|
14
|
+
"packages/zapp-react-native-ui-components/Components/HandlePlayable"
|
|
15
|
+
);
|
|
8
16
|
|
|
9
17
|
const getPlayerModuleProperties = (PlayerModule: ZappPlugin) => {
|
|
10
18
|
if (PlayerModule?.Component && typeof PlayerModule.Component === "object") {
|
|
@@ -52,10 +60,25 @@ export const getPlayer = (
|
|
|
52
60
|
if (type) {
|
|
53
61
|
PlayerModule = findPluginByIdentifier(type, plugins)?.module;
|
|
54
62
|
|
|
63
|
+
if (!PlayerModule) {
|
|
64
|
+
logger.error({
|
|
65
|
+
message:
|
|
66
|
+
"PlayerModule is undefined – type mapping may be wrong or type not set for player",
|
|
67
|
+
data: {
|
|
68
|
+
type,
|
|
69
|
+
screen_id,
|
|
70
|
+
item_type_value: item?.type?.value,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return [null, {}];
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
return getPlayerWithModuleProperties(PlayerModule);
|
|
56
78
|
}
|
|
57
79
|
}
|
|
58
80
|
|
|
81
|
+
// TODO: Probably should be removed, Youtube plugin is deprecated
|
|
59
82
|
if (item?.content?.type === "youtube-id") {
|
|
60
83
|
PlayerModule = findYoutubePlugin(plugins)?.module;
|
|
61
84
|
|
|
@@ -70,5 +93,13 @@ export const getPlayer = (
|
|
|
70
93
|
)
|
|
71
94
|
);
|
|
72
95
|
|
|
96
|
+
if (!PlayerModule) {
|
|
97
|
+
logger.error({
|
|
98
|
+
message: "PlayerModule is undefined – playable plugin not found",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return [null, {}];
|
|
102
|
+
}
|
|
103
|
+
|
|
73
104
|
return getPlayerWithModuleProperties(PlayerModule);
|
|
74
105
|
};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { prepareEntry } from "../index";
|
|
2
|
+
|
|
3
|
+
const makeEntry = (overrides = {}): any => ({
|
|
4
|
+
content: { src: "https://example.com/video.mp4", type: "video/mp4" },
|
|
5
|
+
extensions: {},
|
|
6
|
+
...overrides,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("prepareEntry", () => {
|
|
10
|
+
it("should return null when entry is null or undefined", () => {
|
|
11
|
+
expect(prepareEntry(null)).toBeNull();
|
|
12
|
+
expect(prepareEntry(undefined)).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("preview_playback_override", () => {
|
|
16
|
+
it("should deep merge override with entry, override taking precedence", () => {
|
|
17
|
+
const override = {
|
|
18
|
+
link: { href: "https://video-preload.com/id", rel: "self" },
|
|
19
|
+
extensions: {
|
|
20
|
+
brightcove: { video_id: 1234 },
|
|
21
|
+
preview: true,
|
|
22
|
+
free: true,
|
|
23
|
+
requires_authentication: false,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const entry = makeEntry({
|
|
28
|
+
extensions: {
|
|
29
|
+
preview_playback_override: override,
|
|
30
|
+
existing_field: "keep-me",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const result = prepareEntry(entry);
|
|
35
|
+
|
|
36
|
+
expect(result.extensions.brightcove.video_id).toBe(1234);
|
|
37
|
+
expect(result.extensions.preview).toBe(true);
|
|
38
|
+
expect(result.extensions.existing_field).toBe("keep-me");
|
|
39
|
+
|
|
40
|
+
expect(result.link).toEqual({
|
|
41
|
+
href: "https://video-preload.com/id",
|
|
42
|
+
rel: "self",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.content).toEqual(entry.content);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should override entry fields when both have the same key", () => {
|
|
49
|
+
const override = {
|
|
50
|
+
extensions: {
|
|
51
|
+
brightcove: { video_id: 9999 },
|
|
52
|
+
},
|
|
53
|
+
content: { src: "https://override.com/video.mp4" },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const entry = makeEntry({
|
|
57
|
+
extensions: {
|
|
58
|
+
preview_playback_override: override,
|
|
59
|
+
brightcove: { video_id: 1111, account_id: "abc" },
|
|
60
|
+
},
|
|
61
|
+
content: { src: "https://original.com/video.mp4", type: "video/mp4" },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const result = prepareEntry(entry);
|
|
65
|
+
|
|
66
|
+
expect(result.extensions.brightcove.video_id).toBe(9999);
|
|
67
|
+
expect(result.extensions.brightcove.account_id).toBe("abc");
|
|
68
|
+
expect(result.content.src).toBe("https://override.com/video.mp4");
|
|
69
|
+
expect(result.content.type).toBe("video/mp4");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should take priority over other playback sources", () => {
|
|
73
|
+
const override = {
|
|
74
|
+
link: { href: "https://override.com" },
|
|
75
|
+
extensions: { brightcove: { video_id: 5555 } },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const entry = makeEntry({
|
|
79
|
+
extensions: {
|
|
80
|
+
preview_playback_override: override,
|
|
81
|
+
brightcove: { preview_playback: "other-id" },
|
|
82
|
+
preview_playback: "https://other.com/video.mp4",
|
|
83
|
+
},
|
|
84
|
+
content: {
|
|
85
|
+
src: "https://example.com/video.mp4",
|
|
86
|
+
teaser: "https://teaser.com/video.mp4",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = prepareEntry(entry);
|
|
91
|
+
|
|
92
|
+
expect(result.link).toEqual({ href: "https://override.com" });
|
|
93
|
+
expect(result.extensions.brightcove.video_id).toBe(5555);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("brightcove preview_playback", () => {
|
|
98
|
+
it("should return entry with brightcove video_id replaced by preview_playback", () => {
|
|
99
|
+
const entry = makeEntry({
|
|
100
|
+
extensions: {
|
|
101
|
+
brightcove: {
|
|
102
|
+
video_id: "original-id",
|
|
103
|
+
preview_playback: "preview-id",
|
|
104
|
+
},
|
|
105
|
+
some_other: "value",
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = prepareEntry(entry);
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual({
|
|
112
|
+
...entry,
|
|
113
|
+
extensions: {
|
|
114
|
+
...entry.extensions,
|
|
115
|
+
brightcove: {
|
|
116
|
+
video_id: "preview-id",
|
|
117
|
+
preview_playback: "preview-id",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should preserve other extensions", () => {
|
|
124
|
+
const entry = makeEntry({
|
|
125
|
+
extensions: {
|
|
126
|
+
brightcove: { preview_playback: "preview-id" },
|
|
127
|
+
custom_field: "keep-me",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = prepareEntry(entry);
|
|
132
|
+
|
|
133
|
+
expect(result.extensions.custom_field).toBe("keep-me");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("extensions.preview_playback (non-brightcove)", () => {
|
|
138
|
+
it("should return entry with content.src set to preview_playback URL", () => {
|
|
139
|
+
const entry = makeEntry({
|
|
140
|
+
extensions: { preview_playback: "https://preview.com/video.m3u8" },
|
|
141
|
+
content: { src: "https://original.com/video.mp4" },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = prepareEntry(entry);
|
|
145
|
+
|
|
146
|
+
expect(result.content.src).toBe("https://preview.com/video.m3u8");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should preserve other content fields", () => {
|
|
150
|
+
const entry = makeEntry({
|
|
151
|
+
extensions: { preview_playback: "https://preview.com/video.m3u8" },
|
|
152
|
+
content: { src: "https://original.com/video.mp4", type: "video/mp4" },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = prepareEntry(entry);
|
|
156
|
+
|
|
157
|
+
expect(result.content.type).toBe("video/mp4");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("content.teaser", () => {
|
|
162
|
+
it("should return entry with content.src set to teaser URL", () => {
|
|
163
|
+
const entry = makeEntry({
|
|
164
|
+
extensions: {},
|
|
165
|
+
content: {
|
|
166
|
+
src: "https://original.com/video.mp4",
|
|
167
|
+
teaser: "https://teaser.com/teaser.mp4",
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const result = prepareEntry(entry);
|
|
172
|
+
|
|
173
|
+
expect(result.content.src).toBe("https://teaser.com/teaser.mp4");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("brightcove video_id (without preview_playback)", () => {
|
|
178
|
+
it("should return the entry as-is when brightcove video_id exists", () => {
|
|
179
|
+
const entry = makeEntry({
|
|
180
|
+
extensions: { brightcove: { video_id: "some-id" } },
|
|
181
|
+
content: { src: "" },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const result = prepareEntry(entry);
|
|
185
|
+
|
|
186
|
+
expect(result).toBe(entry);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("content.src fallback", () => {
|
|
191
|
+
it("should return null when content.src is missing", () => {
|
|
192
|
+
const entry = makeEntry({
|
|
193
|
+
extensions: {},
|
|
194
|
+
content: {},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(prepareEntry(entry)).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should return null when content.src is empty", () => {
|
|
201
|
+
const entry = makeEntry({
|
|
202
|
+
extensions: {},
|
|
203
|
+
content: { src: "" },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(prepareEntry(entry)).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should return entry when content.type starts with video", () => {
|
|
210
|
+
const entry = makeEntry({
|
|
211
|
+
extensions: {},
|
|
212
|
+
content: { src: "https://example.com/stream", type: "video/mp4" },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(prepareEntry(entry)).toBe(entry);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should return entry when src has a known video extension", () => {
|
|
219
|
+
const extensions = ["m3u8", "mp4", "m4v", "mkv", "mov", "mpd", "ogv"];
|
|
220
|
+
|
|
221
|
+
for (const ext of extensions) {
|
|
222
|
+
const entry = makeEntry({
|
|
223
|
+
extensions: {},
|
|
224
|
+
content: { src: `https://example.com/video.${ext}` },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(prepareEntry(entry)).toBe(entry);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should return entry for audio extensions", () => {
|
|
232
|
+
const extensions = ["mp3", "oga", "opus"];
|
|
233
|
+
|
|
234
|
+
for (const ext of extensions) {
|
|
235
|
+
const entry = makeEntry({
|
|
236
|
+
extensions: {},
|
|
237
|
+
content: { src: `https://example.com/audio.${ext}` },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(prepareEntry(entry)).toBe(entry);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should return null for non-video/non-audio URLs", () => {
|
|
245
|
+
const entry = makeEntry({
|
|
246
|
+
extensions: {},
|
|
247
|
+
content: { src: "https://example.com/page.html" },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(prepareEntry(entry)).toBeNull();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should return null for URLs without extensions", () => {
|
|
254
|
+
const entry = makeEntry({
|
|
255
|
+
extensions: {},
|
|
256
|
+
content: { src: "https://example.com/stream" },
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(prepareEntry(entry)).toBeNull();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("priority order", () => {
|
|
264
|
+
it("should prefer brightcove.preview_playback over extensions.preview_playback", () => {
|
|
265
|
+
const entry = makeEntry({
|
|
266
|
+
extensions: {
|
|
267
|
+
brightcove: { preview_playback: "bc-preview" },
|
|
268
|
+
preview_playback: "ext-preview",
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const result = prepareEntry(entry);
|
|
273
|
+
|
|
274
|
+
expect(result.extensions.brightcove.video_id).toBe("bc-preview");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should prefer extensions.preview_playback over content.teaser", () => {
|
|
278
|
+
const entry = makeEntry({
|
|
279
|
+
extensions: { preview_playback: "https://preview.com/video.mp4" },
|
|
280
|
+
content: {
|
|
281
|
+
src: "https://original.com/video.mp4",
|
|
282
|
+
teaser: "https://teaser.com/video.mp4",
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const result = prepareEntry(entry);
|
|
287
|
+
|
|
288
|
+
expect(result.content.src).toBe("https://preview.com/video.mp4");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should prefer preview_playback_override over all other sources", () => {
|
|
292
|
+
const override = {
|
|
293
|
+
extensions: { brightcove: { video_id: "override-id" } },
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const entry = makeEntry({
|
|
297
|
+
extensions: {
|
|
298
|
+
preview_playback_override: override,
|
|
299
|
+
brightcove: { preview_playback: "bc-preview", video_id: "original" },
|
|
300
|
+
preview_playback: "https://ext-preview.com/video.mp4",
|
|
301
|
+
},
|
|
302
|
+
content: {
|
|
303
|
+
src: "https://original.com/video.mp4",
|
|
304
|
+
teaser: "https://teaser.com/video.mp4",
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const result = prepareEntry(entry);
|
|
309
|
+
|
|
310
|
+
expect(result.extensions.brightcove.video_id).toBe("override-id");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("should prefer content.teaser over brightcove.video_id", () => {
|
|
314
|
+
const entry = makeEntry({
|
|
315
|
+
extensions: { brightcove: { video_id: "some-id" } },
|
|
316
|
+
content: {
|
|
317
|
+
src: "https://original.com/video.mp4",
|
|
318
|
+
teaser: "https://teaser.com/teaser.mp4",
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const result = prepareEntry(entry);
|
|
323
|
+
|
|
324
|
+
expect(result.content.src).toBe("https://teaser.com/teaser.mp4");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should prefer content.teaser over content.src URL check", () => {
|
|
328
|
+
const entry = makeEntry({
|
|
329
|
+
extensions: {},
|
|
330
|
+
content: {
|
|
331
|
+
src: "https://original.com/video.mp4",
|
|
332
|
+
teaser: "https://teaser.com/teaser.m3u8",
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const result = prepareEntry(entry);
|
|
337
|
+
|
|
338
|
+
expect(result.content.src).toBe("https://teaser.com/teaser.m3u8");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should prefer brightcove.video_id over content.src URL check", () => {
|
|
342
|
+
const entry = makeEntry({
|
|
343
|
+
extensions: { brightcove: { video_id: "bc-id" } },
|
|
344
|
+
content: { src: "https://example.com/page.html" },
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const result = prepareEntry(entry);
|
|
348
|
+
|
|
349
|
+
expect(result).toBe(entry);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
|
|
2
|
+
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
3
|
+
import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
|
|
4
|
+
|
|
5
|
+
const { log_debug } = createLogger({
|
|
6
|
+
category: "LiveImagePreloadHooks",
|
|
7
|
+
subsystem: "LiveVideo",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type PreloadHookConfig = {
|
|
11
|
+
screen_id: string;
|
|
12
|
+
identifier: string;
|
|
13
|
+
type: string;
|
|
14
|
+
weight: number;
|
|
15
|
+
configuration: any;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type HookCallbackResult = {
|
|
19
|
+
success?: boolean;
|
|
20
|
+
error?: Error;
|
|
21
|
+
payload?: any;
|
|
22
|
+
callback?: Function;
|
|
23
|
+
cancelled?: boolean;
|
|
24
|
+
abort?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function runHookWithCallback(
|
|
28
|
+
invoke: (
|
|
29
|
+
payload: any,
|
|
30
|
+
callback: (result: HookCallbackResult) => void
|
|
31
|
+
) => void,
|
|
32
|
+
payload: any
|
|
33
|
+
): Promise<any> {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
invoke(payload, ({ error, payload: resultPayload }: HookCallbackResult) => {
|
|
36
|
+
if (error) {
|
|
37
|
+
reject(error);
|
|
38
|
+
} else {
|
|
39
|
+
resolve(resultPayload ?? payload);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function executePreloadHooks({
|
|
46
|
+
preloadHooks,
|
|
47
|
+
entry,
|
|
48
|
+
}: {
|
|
49
|
+
preloadHooks: PreloadHookConfig[];
|
|
50
|
+
entry: ZappEntry;
|
|
51
|
+
}): Promise<ZappEntry | null> {
|
|
52
|
+
if (!preloadHooks?.length) {
|
|
53
|
+
return entry;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sortedHooks = [...preloadHooks].sort(
|
|
57
|
+
(a, b) => (a.weight || 0) - (b.weight || 0)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
let payload: any = entry;
|
|
61
|
+
|
|
62
|
+
let needsAbort = false;
|
|
63
|
+
const plugins = appStore.get("plugins");
|
|
64
|
+
|
|
65
|
+
for (const hookConfig of sortedHooks) {
|
|
66
|
+
const plugin = findPluginByIdentifier(hookConfig.identifier, plugins, true);
|
|
67
|
+
|
|
68
|
+
if (!plugin?.module) {
|
|
69
|
+
log_debug(
|
|
70
|
+
`Preload hook plugin not found: ${hookConfig.identifier}, skipping`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { module: hookModule } = plugin;
|
|
77
|
+
|
|
78
|
+
if (hookModule.skipHook?.(payload)) {
|
|
79
|
+
log_debug(`Skipping hook: ${hookConfig.identifier}`);
|
|
80
|
+
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (hookModule.runInBackground) {
|
|
85
|
+
log_debug(
|
|
86
|
+
`Running background preload hook: ${hookConfig.identifier}, entry: ${payload?.id} - ${payload?.title}`
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
payload = await runHookWithCallback(
|
|
90
|
+
(item, callback) =>
|
|
91
|
+
hookModule.runInBackground(item, callback, hookConfig, () => {
|
|
92
|
+
needsAbort = true;
|
|
93
|
+
}),
|
|
94
|
+
payload
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (needsAbort) {
|
|
99
|
+
log_debug(
|
|
100
|
+
`Preload hook requested abort: ${hookConfig.identifier}, stopping chain`
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (needsAbort) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return payload;
|
|
112
|
+
}
|