@applicaster/zapp-react-native-utils 16.0.0-rc.9 → 16.0.0-rc.90

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 (201) hide show
  1. package/actionsExecutor/ActionExecutor.ts +57 -12
  2. package/actionsExecutor/ActionExecutorContext.tsx +113 -314
  3. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  4. package/actionsExecutor/__tests__/feedDecorator.test.ts +61 -0
  5. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
  6. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  7. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
  8. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
  9. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +133 -0
  10. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +76 -0
  11. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +212 -0
  12. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  13. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  14. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  15. package/actionsExecutor/actions/appRestart.ts +24 -0
  16. package/actionsExecutor/actions/confirmDialog.ts +53 -0
  17. package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
  18. package/actionsExecutor/actions/goBack.ts +60 -0
  19. package/actionsExecutor/actions/goHome.ts +31 -0
  20. package/actionsExecutor/actions/index.ts +42 -0
  21. package/actionsExecutor/actions/localStorageRemove.ts +27 -0
  22. package/actionsExecutor/actions/localStorageSet.ts +28 -0
  23. package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
  24. package/actionsExecutor/actions/navigateToScreen.ts +137 -0
  25. package/actionsExecutor/actions/openBottomSheet.ts +329 -0
  26. package/actionsExecutor/actions/presentScreen.ts +78 -0
  27. package/actionsExecutor/actions/refreshComponent.ts +85 -0
  28. package/actionsExecutor/actions/screenSetVariable.ts +35 -0
  29. package/actionsExecutor/actions/screenToggleFlag.ts +38 -0
  30. package/actionsExecutor/actions/sendCloudEvent.ts +93 -0
  31. package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
  32. package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
  33. package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
  34. package/actionsExecutor/actions/showAlert.ts +51 -0
  35. package/actionsExecutor/actions/showToast.ts +87 -0
  36. package/actionsExecutor/actions/switchLayout.ts +40 -0
  37. package/actionsExecutor/consts.ts +27 -0
  38. package/actionsExecutor/feedDecorator.ts +6 -6
  39. package/actionsExecutor/types.ts +59 -0
  40. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
  41. package/analyticsUtils/PlayerAnalyticsManager.ts +12 -2
  42. package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
  43. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
  44. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
  45. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
  46. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
  47. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
  48. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
  49. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
  50. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
  51. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
  52. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
  53. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
  54. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
  55. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
  56. package/analyticsUtils/__tests__/fixtures/index.js +20 -0
  57. package/analyticsUtils/analyticsMapper.ts +4 -1
  58. package/analyticsUtils/playerAnalyticsTracker.ts +26 -3
  59. package/appUtils/HooksManager/index.ts +12 -8
  60. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -1
  61. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -1
  62. package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
  63. package/appUtils/contextKeysManager/index.ts +1 -1
  64. package/appUtils/contextKeysManager/utils/index.ts +38 -25
  65. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +143 -53
  66. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  67. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +49 -0
  68. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  69. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  70. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +11 -0
  71. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  72. package/appUtils/playerManager/OverlayObserver/utils.ts +54 -20
  73. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  74. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  75. package/appUtils/playerManager/__tests__/playerNative.test.ts +38 -0
  76. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  77. package/appUtils/playerManager/{conts.ts → const.ts} +20 -0
  78. package/appUtils/playerManager/index.ts +1 -1
  79. package/appUtils/playerManager/player.ts +115 -3
  80. package/appUtils/playerManager/playerFactory.ts +1 -1
  81. package/appUtils/playerManager/playerNative.ts +6 -4
  82. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  83. package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
  84. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  85. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  86. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  87. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  88. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  89. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  90. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  91. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  92. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  93. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  94. package/arrayUtils/__tests__/reorderByIds.test.ts +50 -0
  95. package/arrayUtils/__tests__/sample.test.ts +61 -0
  96. package/arrayUtils/index.ts +136 -20
  97. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  98. package/bottomSheet/index.ts +17 -0
  99. package/cellUtils/index.ts +33 -8
  100. package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
  101. package/colorUtils/__tests__/isValidColor.test.ts +70 -0
  102. package/colorUtils/index.ts +50 -0
  103. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  104. package/configurationUtils/__tests__/modalBlocksParser.test.ts +260 -0
  105. package/configurationUtils/manifestKeyParser.ts +50 -10
  106. package/configurationUtils/modalBlocksParser.ts +296 -0
  107. package/entryActions/__tests__/buildEntryActions.test.ts +195 -0
  108. package/entryActions/aliasPresentation.ts +124 -0
  109. package/entryActions/buildEntryActions.ts +167 -0
  110. package/entryActions/index.ts +17 -0
  111. package/entryActions/types.ts +39 -0
  112. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  113. package/manifestUtils/_internals/index.js +14 -3
  114. package/manifestUtils/defaultManifestConfigurations/generalContent.js +41 -0
  115. package/manifestUtils/defaultManifestConfigurations/player.js +349 -20
  116. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
  117. package/manifestUtils/fieldUtils/index.js +125 -0
  118. package/manifestUtils/index.js +3 -0
  119. package/manifestUtils/keys.js +9 -0
  120. package/manifestUtils/mobileAction/button/index.js +16 -0
  121. package/manifestUtils/mobileAction/container/index.js +3 -1
  122. package/manifestUtils/mobileAction/groups/defaults.js +3 -1
  123. package/manifestUtils/modalBlocks.js +304 -0
  124. package/manifestUtils/platformIsTV.js +1 -0
  125. package/modalState/ContentViewModel.ts +113 -0
  126. package/modalState/EditableCollection.ts +17 -0
  127. package/modalState/EditableCollectionRegistry.ts +51 -0
  128. package/modalState/ModalOrchestrator.ts +211 -0
  129. package/modalState/RemoteEditableCollection.ts +235 -0
  130. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  131. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  132. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  133. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +47 -0
  134. package/modalState/__tests__/RemoteEditableCollection.test.ts +415 -0
  135. package/modalState/__tests__/index.test.ts +30 -1
  136. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  137. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  138. package/modalState/index.ts +35 -83
  139. package/modalState/store.ts +109 -0
  140. package/modalState/types.ts +160 -0
  141. package/modalState/useBottomSheetContent.ts +109 -0
  142. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  143. package/numberUtils/__tests__/isZero.test.ts +27 -0
  144. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  145. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  146. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  147. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  148. package/numberUtils/index.ts +27 -8
  149. package/package.json +2 -2
  150. package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
  151. package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
  152. package/pipesUtils/index.ts +1 -0
  153. package/pipesUtils/withPipesEndpoint.tsx +54 -0
  154. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  155. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  156. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  157. package/playerUtils/contentDataKeys.ts +134 -0
  158. package/playerUtils/index.ts +41 -18
  159. package/playerUtils/isAudioItem.ts +26 -0
  160. package/playerUtils/resolvePlayerActions.ts +71 -0
  161. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  162. package/reactHooks/actions/index.ts +117 -2
  163. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  164. package/reactHooks/cell-click/index.ts +39 -24
  165. package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
  166. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  167. package/reactHooks/feed/index.ts +5 -1
  168. package/reactHooks/feed/useBatchLoading.ts +9 -1
  169. package/reactHooks/feed/{usePipesCacheReset.ts → useMarkPipesDataStale.ts} +12 -4
  170. package/reactHooks/index.ts +2 -0
  171. package/reactHooks/layout/index.ts +1 -1
  172. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  173. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  174. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  175. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  176. package/reactHooks/navigation/index.ts +3 -1
  177. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  178. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  179. package/reactHooks/navigation/useRoute.ts +22 -5
  180. package/reactHooks/state/useComponentScreenState.ts +1 -1
  181. package/reactHooks/usePluginConfiguration.ts +4 -1
  182. package/reactHooks/utils/__tests__/index.test.js +22 -2
  183. package/reactHooks/utils/index.ts +13 -2
  184. package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +22 -4
  185. package/riverComponetsMeasurementProvider/index.tsx +12 -8
  186. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  187. package/stringUtils/index.ts +2 -2
  188. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  189. package/uiActionsRegistrator/__tests__/subscribe.test.ts +107 -0
  190. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  191. package/uiActionsRegistrator/index.ts +25 -0
  192. package/uiActionsRegistrator/registry.ts +335 -0
  193. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  194. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  195. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  196. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
  197. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  198. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  199. package/playerUtils/getPlayerActionButtons.ts +0 -17
  200. package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
  201. /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
@@ -0,0 +1,403 @@
1
+ import { Player } from "../player";
2
+ import { isTV } from "../../../reactUtils";
3
+
4
+ jest.mock("../../../reactUtils", () => ({
5
+ ...jest.requireActual("../../../reactUtils"),
6
+ isTV: jest.fn(() => false),
7
+ }));
8
+
9
+ const mockIsTV = isTV as jest.Mock;
10
+
11
+ const ENTRY = {
12
+ title: "Entry Title",
13
+ summary: "Entry Summary",
14
+ extensions: { programme: "Programme Title" },
15
+ } as unknown as ZappEntry;
16
+
17
+ const LIVE_ENTRY = {
18
+ title: "Live Title",
19
+ summary: "Live Summary",
20
+ } as unknown as ZappEntry;
21
+
22
+ const makePlayer = (configuration: Record<string, any> | null = null) => {
23
+ const player = new Player({ playerId: "test-player" });
24
+
25
+ player.getPluginConfiguration = () => configuration;
26
+
27
+ return player;
28
+ };
29
+
30
+ describe("Player content channel", () => {
31
+ beforeEach(() => {
32
+ mockIsTV.mockReturnValue(false);
33
+ });
34
+
35
+ it("resolves the entry fields when nothing is configured", () => {
36
+ const player = makePlayer();
37
+
38
+ player.entry = ENTRY;
39
+
40
+ expect(player.getContent()).toEqual({
41
+ title: "Entry Title",
42
+ summary: "Entry Summary",
43
+ });
44
+ });
45
+
46
+ it("resolves a configured custom data key", () => {
47
+ const player = makePlayer({
48
+ title_data_key_source: "other",
49
+ title_custom_data_key: "extensions.programme",
50
+ });
51
+
52
+ player.entry = ENTRY;
53
+
54
+ expect(player.getContent().title).toBe("Programme Title");
55
+ });
56
+
57
+ it("falls back to the entry field when the configured key is unresolvable", () => {
58
+ const player = makePlayer({
59
+ title_data_key_source: "other",
60
+ title_custom_data_key: "extensions.nope",
61
+ });
62
+
63
+ player.entry = ENTRY;
64
+
65
+ expect(player.getContent().title).toBe("Entry Title");
66
+ });
67
+
68
+ it("emits the resolved content to subscribers when the entry changes", () => {
69
+ const player = makePlayer();
70
+ const received: any[] = [];
71
+
72
+ player
73
+ .getContentObservable()
74
+ .subscribe((content) => received.push(content));
75
+
76
+ player.entry = ENTRY;
77
+
78
+ expect(received[received.length - 1]).toEqual({
79
+ title: "Entry Title",
80
+ summary: "Entry Summary",
81
+ });
82
+ });
83
+
84
+ it("seeds subscribers with resolved content, never the raw entry", () => {
85
+ const player = makePlayer({
86
+ title_data_key_source: "other",
87
+ title_custom_data_key: "extensions.programme",
88
+ });
89
+
90
+ player.entry = ENTRY;
91
+
92
+ const received: any[] = [];
93
+
94
+ player
95
+ .getContentObservable()
96
+ .subscribe((content) => received.push(content));
97
+
98
+ expect(received[0].title).toBe("Programme Title");
99
+ });
100
+
101
+ // The live entry lives on `OverlaysObserver`, not on the player, so these
102
+ // stub the observer rather than constructing a real one (whose constructor
103
+ // pulls in chapter markers, play-next and the plugin store). What is under
104
+ // test here is only how `getContent` and the `entry` setter treat it; the
105
+ // observer's own side - loading the feed and storing the result - is
106
+ // covered in `OverlayObserver/__tests__/liveContent.test.ts`.
107
+ const withLiveEntry = (player: Player, liveEntry: ZappEntry | null) => {
108
+ (player as any).overlaysObserver = {
109
+ liveEntry,
110
+ clearLiveEntry() {
111
+ this.liveEntry = null;
112
+ },
113
+ };
114
+ };
115
+
116
+ it("prefers the observer's live entry over the player entry", () => {
117
+ const player = makePlayer();
118
+
119
+ player.entry = ENTRY;
120
+ withLiveEntry(player, LIVE_ENTRY);
121
+
122
+ expect(player.getContent()).toEqual({
123
+ title: "Live Title",
124
+ summary: "Live Summary",
125
+ });
126
+ });
127
+
128
+ it("clears the live entry when a new entry is set", () => {
129
+ const player = makePlayer();
130
+
131
+ player.entry = ENTRY;
132
+ withLiveEntry(player, LIVE_ENTRY);
133
+ player.entry = ENTRY;
134
+
135
+ expect(player.getContent().title).toBe("Entry Title");
136
+ });
137
+
138
+ it("falls through to the player entry when there is no observer at all", () => {
139
+ const player = makePlayer();
140
+
141
+ player.entry = ENTRY;
142
+
143
+ expect((player as any).overlaysObserver).toBeFalsy();
144
+ expect(player.getContent().title).toBe("Entry Title");
145
+ });
146
+
147
+ it("resolves the cover image from the entry's media group", () => {
148
+ const player = makePlayer();
149
+
150
+ player.entry = {
151
+ title: "Entry Title",
152
+ media_group: [
153
+ {
154
+ type: "image",
155
+ media_item: [
156
+ { key: "image_other", src: "https://img.test/other.jpg" },
157
+ { key: "image_base", src: "https://img.test/base.jpg" },
158
+ ],
159
+ },
160
+ ],
161
+ } as unknown as ZappEntry;
162
+
163
+ expect(player.getContent().imageUrl).toBe("https://img.test/base.jpg");
164
+ });
165
+
166
+ it("falls back to the first media item when there is no image_base", () => {
167
+ const player = makePlayer();
168
+
169
+ player.entry = {
170
+ title: "Entry Title",
171
+ media_group: [
172
+ {
173
+ type: "image",
174
+ media_item: [
175
+ { key: "image_other", src: "https://img.test/other.jpg" },
176
+ ],
177
+ },
178
+ ],
179
+ } as unknown as ZappEntry;
180
+
181
+ expect(player.getContent().imageUrl).toBe("https://img.test/other.jpg");
182
+ });
183
+
184
+ it("has no image when the entry carries no media group", () => {
185
+ const player = makePlayer();
186
+
187
+ player.entry = ENTRY;
188
+
189
+ expect(player.getContent().imageUrl).toBeUndefined();
190
+ });
191
+
192
+ it("takes the cover image from the live entry, not the channel", () => {
193
+ // The programme's own artwork has to travel with its title - otherwise the
194
+ // lock screen would keep showing the channel's cover all day.
195
+ const player = makePlayer();
196
+
197
+ player.entry = {
198
+ title: "Channel",
199
+ media_group: [
200
+ {
201
+ type: "image",
202
+ media_item: [
203
+ { key: "image_base", src: "https://img.test/channel.jpg" },
204
+ ],
205
+ },
206
+ ],
207
+ } as unknown as ZappEntry;
208
+
209
+ withLiveEntry(player, {
210
+ title: "Programme",
211
+ media_group: [
212
+ {
213
+ type: "image",
214
+ media_item: [
215
+ { key: "image_base", src: "https://img.test/programme.jpg" },
216
+ ],
217
+ },
218
+ ],
219
+ } as unknown as ZappEntry);
220
+
221
+ expect(player.getContent().imageUrl).toBe("https://img.test/programme.jpg");
222
+ });
223
+
224
+ it("publishes the observer's live entry to subscribers", () => {
225
+ const player = makePlayer();
226
+ const received: any[] = [];
227
+
228
+ player.entry = ENTRY;
229
+
230
+ player
231
+ .getContentObservable()
232
+ .subscribe((content) => received.push(content));
233
+
234
+ withLiveEntry(player, LIVE_ENTRY);
235
+ player.publishContent();
236
+
237
+ expect(received[received.length - 1].title).toBe("Live Title");
238
+ });
239
+
240
+ it("does not resolve while no subscriber exists", () => {
241
+ const player = makePlayer();
242
+ const getConfiguration = jest.fn(() => null);
243
+
244
+ player.getPluginConfiguration = getConfiguration;
245
+ player.entry = ENTRY;
246
+
247
+ expect(getConfiguration).not.toHaveBeenCalled();
248
+ });
249
+ });
250
+
251
+ /**
252
+ * Which Studio key set backs the resolution is decided inside `getContent()`
253
+ * (via `getPlayerContentDataKeys`). Getting it wrong would make one platform
254
+ * or content type read another's Studio configuration, so each combination is
255
+ * pinned here - including the negative cases that prove the sets never cross.
256
+ *
257
+ * These moved here from `PlayerContainer.test.tsx` when the resolution moved
258
+ * out of the React tree and into the player; the assertions are unchanged.
259
+ */
260
+ describe("Player content channel: key-set selection", () => {
261
+ const VIDEO_ENTRY = {
262
+ id: "video-entry",
263
+ title: "Entry Title",
264
+ summary: "Entry Summary",
265
+ content: { src: "https://example.com/video.m3u8" },
266
+ extensions: { customTitleKey: "Custom Title From Extensions" },
267
+ } as unknown as ZappEntry;
268
+
269
+ const AUDIO_ENTRY = {
270
+ id: "audio-entry",
271
+ title: "Entry Title",
272
+ summary: "Entry Summary",
273
+ content: { type: "audio", src: "https://example.com/audio.m3u8" },
274
+ extensions: { customTitleKey: "Custom Audio Title From Extensions" },
275
+ } as unknown as ZappEntry;
276
+
277
+ const contentFor = (
278
+ entry: ZappEntry,
279
+ configuration: Record<string, any> | null
280
+ ) => {
281
+ const player = makePlayer(configuration);
282
+
283
+ player.entry = entry;
284
+
285
+ return player.getContent();
286
+ };
287
+
288
+ describe("TV video", () => {
289
+ beforeEach(() => mockIsTV.mockReturnValue(true));
290
+
291
+ it("resolves the TV video override to the configured extensions key", () => {
292
+ const content = contentFor(VIDEO_ENTRY, {
293
+ video_player_title_data_key_source: "other",
294
+ video_player_title_custom_data_key: "extensions.customTitleKey",
295
+ });
296
+
297
+ expect(content.title).toBe("Custom Title From Extensions");
298
+ // summary was not overridden, so it keeps falling back to entry.summary.
299
+ expect(content.summary).toBe("Entry Summary");
300
+ });
301
+
302
+ it("falls back to the entry title when the configured key does not resolve", () => {
303
+ const content = contentFor(VIDEO_ENTRY, {
304
+ video_player_title_data_key_source: "other",
305
+ video_player_title_custom_data_key: "extensions.doesNotExist",
306
+ });
307
+
308
+ expect(content.title).toBe("Entry Title");
309
+ });
310
+
311
+ it("ignores mobile configuration", () => {
312
+ const content = contentFor(VIDEO_ENTRY, {
313
+ title_data_key_source: "other",
314
+ title_custom_data_key: "extensions.customTitleKey",
315
+ });
316
+
317
+ expect(content.title).toBe("Entry Title");
318
+ });
319
+
320
+ it("reads the TV video key set, not the mobile one, when both are configured", () => {
321
+ const content = contentFor(VIDEO_ENTRY, {
322
+ video_player_title_data_key_source: "other",
323
+ video_player_title_custom_data_key: "extensions.customTitleKey",
324
+ // A mobile key is also configured, to prove the TV branch ignores it.
325
+ title_data_key_source: "other",
326
+ title_custom_data_key: "extensions.doesNotExist",
327
+ });
328
+
329
+ expect(content.title).toBe("Custom Title From Extensions");
330
+ });
331
+
332
+ it("passes the entry fields through when nothing is configured", () => {
333
+ const content = contentFor(VIDEO_ENTRY, {});
334
+
335
+ expect(content.title).toBe("Entry Title");
336
+ expect(content.summary).toBe("Entry Summary");
337
+ });
338
+ });
339
+
340
+ describe("TV audio", () => {
341
+ beforeEach(() => mockIsTV.mockReturnValue(true));
342
+
343
+ it("reads the TV audio key set, not the TV video one", () => {
344
+ const content = contentFor(AUDIO_ENTRY, {
345
+ audio_player_title_data_key_source: "other",
346
+ audio_player_title_custom_data_key: "extensions.customTitleKey",
347
+ // Configuring the TV *video* key too, to prove TV audio content reads
348
+ // its own key family instead.
349
+ video_player_title_data_key_source: "other",
350
+ video_player_title_custom_data_key: "extensions.doesNotExist",
351
+ });
352
+
353
+ expect(content.title).toBe("Custom Audio Title From Extensions");
354
+ });
355
+
356
+ it("falls back to the entry fields when nothing is configured", () => {
357
+ const content = contentFor(AUDIO_ENTRY, {});
358
+
359
+ expect(content.title).toBe("Entry Title");
360
+ expect(content.summary).toBe("Entry Summary");
361
+ });
362
+ });
363
+
364
+ describe("mobile", () => {
365
+ beforeEach(() => mockIsTV.mockReturnValue(false));
366
+
367
+ it("resolves the default (mobile) key set", () => {
368
+ const content = contentFor(VIDEO_ENTRY, {
369
+ title_data_key_source: "other",
370
+ title_custom_data_key: "extensions.customTitleKey",
371
+ });
372
+
373
+ expect(content.title).toBe("Custom Title From Extensions");
374
+ });
375
+
376
+ it("ignores TV video configuration", () => {
377
+ const content = contentFor(VIDEO_ENTRY, {
378
+ video_player_title_data_key_source: "other",
379
+ video_player_title_custom_data_key: "extensions.customTitleKey",
380
+ });
381
+
382
+ expect(content.title).toBe("Entry Title");
383
+ });
384
+
385
+ it("shares one key set between video and audio", () => {
386
+ const content = contentFor(AUDIO_ENTRY, {
387
+ title_data_key_source: "other",
388
+ title_custom_data_key: "extensions.customTitleKey",
389
+ });
390
+
391
+ expect(content.title).toBe("Custom Audio Title From Extensions");
392
+ });
393
+
394
+ it("ignores TV audio configuration for mobile audio", () => {
395
+ const content = contentFor(AUDIO_ENTRY, {
396
+ audio_player_title_data_key_source: "other",
397
+ audio_player_title_custom_data_key: "extensions.customTitleKey",
398
+ });
399
+
400
+ expect(content.title).toBe("Entry Title");
401
+ });
402
+ });
403
+ });
@@ -10,7 +10,7 @@ jest.mock("@applicaster/zapp-react-native-utils/pluginUtils", () => ({
10
10
  mockFindPluginByIdentifier(id, plugins),
11
11
  }));
12
12
 
13
- import { PlayerRole } from "../conts";
13
+ import { PlayerRole } from "../const";
14
14
  import { playerFactory } from "../playerFactory";
15
15
 
16
16
  const fakeComponent = () => null;
@@ -0,0 +1,38 @@
1
+ import { PlayerNative } from "../playerNative";
2
+
3
+ const playerWithEntry = (entry: any) =>
4
+ new PlayerNative({
5
+ entry,
6
+ player: null as any,
7
+ playerId: "test-player",
8
+ });
9
+
10
+ describe("PlayerNative.isAudioItem", () => {
11
+ it.each(["audio", "audio-player", "audio-detail", "audio-show", "audio-hub"])(
12
+ "treats the %s content type as audio",
13
+ (type) => {
14
+ expect(playerWithEntry({ type: { value: type } }).isAudioItem()).toBe(
15
+ true
16
+ );
17
+ }
18
+ );
19
+
20
+ it.each(["video", "video-detail", "comic-detail", "article-show"])(
21
+ "does not treat the %s content type as audio",
22
+ (type) => {
23
+ expect(playerWithEntry({ type: { value: type } }).isAudioItem()).toBe(
24
+ false
25
+ );
26
+ }
27
+ );
28
+
29
+ it("falls back to the media type when the content type says nothing", () => {
30
+ const entry = { content: { type: "audio/mpeg" }, type: { value: "link" } };
31
+
32
+ expect(playerWithEntry(entry).isAudioItem()).toBe(true);
33
+ });
34
+
35
+ it("returns false when there is no entry", () => {
36
+ expect(playerWithEntry(null).isAudioItem()).toBe(false);
37
+ });
38
+ });
@@ -0,0 +1,64 @@
1
+ import { renderHook, act } from "@testing-library/react-native";
2
+ import { BehaviorSubject } from "rxjs";
3
+
4
+ import { usePlayerContent } from "../usePlayerContent";
5
+
6
+ const mockPlayer: any = {
7
+ subject: null as BehaviorSubject<any> | null,
8
+ getContent: jest.fn(),
9
+ getContentObservable: jest.fn(),
10
+ };
11
+
12
+ jest.mock("../usePlayer", () => ({
13
+ usePlayer: () => mockPlayer,
14
+ }));
15
+
16
+ describe("usePlayerContent", () => {
17
+ beforeEach(() => {
18
+ mockPlayer.subject = new BehaviorSubject({
19
+ title: "First Title",
20
+ summary: "First Summary",
21
+ });
22
+
23
+ mockPlayer.getContent = jest.fn(() => mockPlayer.subject.getValue());
24
+
25
+ mockPlayer.getContentObservable = jest.fn(() =>
26
+ mockPlayer.subject.asObservable()
27
+ );
28
+ });
29
+
30
+ it("returns the player's current content", () => {
31
+ const { result } = renderHook(() => usePlayerContent());
32
+
33
+ expect(result.current).toEqual({
34
+ title: "First Title",
35
+ summary: "First Summary",
36
+ });
37
+ });
38
+
39
+ it("re-renders when the player publishes new content", () => {
40
+ const { result } = renderHook(() => usePlayerContent());
41
+
42
+ act(() => {
43
+ mockPlayer.subject.next({
44
+ title: "Second Title",
45
+ summary: "Second Summary",
46
+ });
47
+ });
48
+
49
+ expect(result.current).toEqual({
50
+ title: "Second Title",
51
+ summary: "Second Summary",
52
+ });
53
+ });
54
+
55
+ it("unsubscribes on unmount", () => {
56
+ const { unmount } = renderHook(() => usePlayerContent());
57
+
58
+ expect(mockPlayer.subject.observed).toBe(true);
59
+
60
+ unmount();
61
+
62
+ expect(mockPlayer.subject.observed).toBe(false);
63
+ });
64
+ });
@@ -67,3 +67,23 @@ export enum PlayerRole {
67
67
  Chromecast = "Chromecast",
68
68
  Unspecified = "Unspecified",
69
69
  }
70
+
71
+ /**
72
+ * The playback engine backing a web player. Published on
73
+ * `window.applicaster.playerType` when a player is created, so that plugins
74
+ * observing the player can tell which engine they are looking at instead of
75
+ * inferring it from the API the instance happens to expose.
76
+ *
77
+ * Keep in sync with `ApplicasterGlobals["playerType"]` in applicaster-types.
78
+ */
79
+ export enum WebPlayerEngine {
80
+ Videojs = "videojs",
81
+ Hlsjs = "hlsjs",
82
+ Dashjs = "dashjs",
83
+ }
84
+
85
+ export enum PLAYER_EVENTS {
86
+ PlayNextTriggered = "play_next_triggered",
87
+ }
88
+
89
+ export type PlayNextTriggeredEvent = { id: string };
@@ -9,7 +9,7 @@ import {
9
9
  unsubscribeToDeviceNetworkChanges,
10
10
  } from "@applicaster/quick-brick-core/App/NetworkStatusProvider/utils";
11
11
  import { setUserLanguagePreference } from "./userLanguagePreference";
12
- import { PlayerRole } from "./conts";
12
+ import { PlayerRole } from "./const";
13
13
  import { RNPlayerManager } from "./nativePlayerManager";
14
14
 
15
15
  const { log_error, log_debug, log_warning } = createLogger({
@@ -1,9 +1,22 @@
1
1
  import * as R from "ramda";
2
- import { BehaviorSubject } from "rxjs";
2
+ import { BehaviorSubject, Observable } from "rxjs";
3
+ import { distinctUntilChanged } from "rxjs/operators";
3
4
  import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils";
4
5
  import { createLogger, utilsLogger } from "../../logger";
5
- import { getBoolFromConfigValue } from "../../configurationUtils";
6
- import { PlayerRole, SharedPlayerCallBacksKeys } from "./conts";
6
+ import {
7
+ getBoolFromConfigValue,
8
+ imageSrcFromMediaItem,
9
+ } from "../../configurationUtils";
10
+ // Imported from the leaf modules rather than the `playerUtils` barrel: that
11
+ // barrel imports `playerManager`, so going through it would create a require
12
+ // cycle (playerManager/index -> player -> playerUtils/index -> playerManager).
13
+ import {
14
+ resolvePlayerTitleSubtitle,
15
+ getPlayerContentDataKeys,
16
+ } from "../../playerUtils/resolvePlayerTitleSubtitle";
17
+ import { isAudioItem } from "../../playerUtils/isAudioItem";
18
+ import { isTV } from "../../reactUtils";
19
+ import { PlayerRole, SharedPlayerCallBacksKeys } from "./const";
7
20
  import { OverlaysObserver } from "./OverlayObserver/OverlaysObserver";
8
21
  import { TVSeekController } from "../../reactHooks/player/TVSeekControlller/TVSeekController";
9
22
 
@@ -19,6 +32,23 @@ export declare type PlayerControllerListenerData = {
19
32
  id: string;
20
33
  listener: QuickBrickPlayer.SharedPlayerCallBacks;
21
34
  };
35
+
36
+ /**
37
+ * The player's resolved Title/Subtitle text, published by the player the same
38
+ * way duration and progress are. Consumers render these values as-is rather
39
+ * than reading `entry` or the plugin configuration themselves.
40
+ */
41
+ export type PlayerContent = {
42
+ title: Option<string | number>;
43
+ summary: Option<string | number>;
44
+ /**
45
+ * Cover art for the content currently playing. Published alongside the text
46
+ * (rather than read from `entry` by whoever needs it) so it follows the same
47
+ * entry: on live audio that means the programme's own artwork, refreshed
48
+ * with every feed poll, instead of the channel's.
49
+ */
50
+ imageUrl: Option<string>;
51
+ };
22
52
  /**
23
53
  * Player Instance Controller
24
54
  * Instance exposes utility methods that will help set or get state
@@ -45,6 +75,7 @@ export class Player {
45
75
  readonly playerRole: PlayerRole = PlayerRole.Unspecified;
46
76
  private overlaysObserver?: OverlaysObserver | null;
47
77
  private entryObservable?: BehaviorSubject<ZappEntry> | null;
78
+ private contentSubject?: BehaviorSubject<PlayerContent> | null;
48
79
 
49
80
  protected listeners: {
50
81
  [key: string]: QuickBrickPlayer.SharedPlayerCallBacks;
@@ -58,7 +89,17 @@ export class Player {
58
89
 
59
90
  set entry(newEntry: ZappEntry | null) {
60
91
  this._entry = newEntry;
92
+ // The live entry is scoped to the entry it overlays - see
93
+ // `OverlaysObserver.clearLiveEntry`. No-op when there is no observer,
94
+ // which is every non-live player.
95
+ this.overlaysObserver?.clearLiveEntry();
61
96
  this.entryObservable?.next(newEntry);
97
+ // Optional chaining short-circuits the whole call expression, so
98
+ // `getContent()` is NOT evaluated while no subscriber exists. That
99
+ // matters: `PlayerNative` assigns `this.entry` in its constructor, before
100
+ // subclass arrow-function properties such as `getPluginConfiguration`
101
+ // have been assigned.
102
+ this.contentSubject?.next(this.getContent());
62
103
  }
63
104
 
64
105
  constructor(props) {
@@ -117,6 +158,77 @@ export class Player {
117
158
  return this.entryObservable;
118
159
  };
119
160
 
161
+ /**
162
+ * The player's resolved Title/Subtitle.
163
+ *
164
+ * Resolved on demand rather than cached: the plugin configuration arrives
165
+ * asynchronously on both the native and cast paths, so a value computed once
166
+ * at entry-set time would be stale for whichever arrives first.
167
+ *
168
+ * Every fallback (no config, empty/whitespace key, unresolvable key, key
169
+ * resolving to an empty string or to a non-string/non-number) lives in
170
+ * `resolvePlayerContentText` and is never reimplemented here.
171
+ */
172
+ getContent = (): PlayerContent => {
173
+ // Live audio keeps playing one channel while the programme on air
174
+ // changes, so the observer's freshly-loaded programme entry wins when
175
+ // there is one. Read through `?.` so this never constructs an observer:
176
+ // a VOD or cast player has none and falls straight through to its entry.
177
+ const entry = this.overlaysObserver?.liveEntry ?? this.entry;
178
+
179
+ return {
180
+ ...resolvePlayerTitleSubtitle({
181
+ entry,
182
+ pluginConfiguration: this.getPluginConfiguration(),
183
+ dataKeys: getPlayerContentDataKeys({
184
+ isTV: isTV(),
185
+ isAudioContent: isAudioItem(entry),
186
+ }),
187
+ }),
188
+ // Same selection Android's `EntryHelpers.extractCoverImage` makes:
189
+ // prefer the `image_base` media item, otherwise the first one available.
190
+ // Reused rather than reimplemented so the two platforms cannot drift.
191
+ imageUrl: entry ? imageSrcFromMediaItem(entry, []) : undefined,
192
+ };
193
+ };
194
+
195
+ /**
196
+ * Created lazily, like `entryObservable`. An eagerly created subject would
197
+ * resolve during `PlayerNative`'s constructor and call a not-yet-assigned
198
+ * `getPluginConfiguration`.
199
+ *
200
+ * The seed is produced by calling the resolver, never from raw entry fields:
201
+ * a `BehaviorSubject` replays its seed synchronously on subscribe, so a raw
202
+ * seed would overwrite correctly-resolved text at mount.
203
+ */
204
+ getContentObservable = (): Observable<PlayerContent> => {
205
+ if (!this.contentSubject) {
206
+ this.contentSubject = new BehaviorSubject<PlayerContent>(
207
+ this.getContent()
208
+ );
209
+ }
210
+
211
+ return this.contentSubject.asObservable().pipe(
212
+ // `resolvePlayerTitleSubtitle` returns a fresh object every call, so
213
+ // reference equality would never dedupe. Compare by value.
214
+ distinctUntilChanged(
215
+ (previous, next) =>
216
+ previous.title === next.title &&
217
+ previous.summary === next.summary &&
218
+ previous.imageUrl === next.imageUrl
219
+ )
220
+ );
221
+ };
222
+
223
+ /**
224
+ * Re-resolves and publishes the current content. Called when something the
225
+ * resolution depends on changed without `entry` itself being reassigned -
226
+ * today that is the observer swapping in a newly-loaded live programme.
227
+ */
228
+ publishContent = () => {
229
+ this.contentSubject?.next(this.getContent());
230
+ };
231
+
120
232
  /**
121
233
  * ---------------------------------------- PLAYER SETTERS GETTERS ----------------------------------------
122
234
  * These methods let you get information about the content / ad playing