@applicaster/zapp-react-native-ui-components 15.0.0-rc.14 → 15.0.0-rc.140

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 (197) hide show
  1. package/Components/AnimatedInOut/index.tsx +69 -26
  2. package/Components/BaseFocusable/index.ios.ts +12 -2
  3. package/Components/Cell/Cell.tsx +14 -3
  4. package/Components/Cell/CellWithFocusable.tsx +9 -0
  5. package/Components/Cell/FocusableWrapper.tsx +3 -0
  6. package/Components/Cell/TvOSCellComponent.tsx +25 -6
  7. package/Components/Focusable/Focusable.tsx +4 -2
  8. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  9. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  10. package/Components/FocusableGroup/FocusableTvOS.tsx +32 -1
  11. package/Components/GeneralContentScreen/GeneralContentScreen.tsx +39 -28
  12. package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
  13. package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
  14. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  15. package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
  16. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +22 -6
  17. package/Components/HandlePlayable/HandlePlayable.tsx +33 -94
  18. package/Components/HandlePlayable/const.ts +3 -0
  19. package/Components/HandlePlayable/utils.ts +105 -0
  20. package/Components/HookRenderer/HookRenderer.tsx +40 -10
  21. package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
  22. package/Components/Layout/TV/LayoutBackground.tsx +5 -2
  23. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  24. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  25. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  26. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  27. package/Components/Layout/TV/index.tsx +3 -4
  28. package/Components/Layout/TV/index.web.tsx +3 -4
  29. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  30. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  31. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  32. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  33. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  34. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  35. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  36. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  37. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  38. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  39. package/Components/MasterCell/DefaultComponents/BorderContainerView/__tests__/index.test.tsx +16 -1
  40. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +30 -2
  41. package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
  42. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +5 -1
  43. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  44. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  45. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  46. package/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
  47. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +136 -0
  48. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +43 -22
  49. package/Components/MasterCell/DefaultComponents/PressableView.tsx +261 -0
  50. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +40 -39
  51. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/Image.test.tsx +95 -0
  52. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +86 -0
  53. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/index.test.ts +141 -0
  54. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  55. package/Components/MasterCell/DefaultComponents/SecondaryImage/index.ts +1 -1
  56. package/Components/MasterCell/DefaultComponents/Text/index.tsx +10 -14
  57. package/Components/MasterCell/DefaultComponents/index.ts +2 -0
  58. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +42 -0
  59. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +127 -0
  60. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ButtonContainerView.ts +23 -0
  61. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  62. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  63. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +32 -0
  64. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +195 -0
  65. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +140 -0
  66. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +222 -0
  67. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  68. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +104 -0
  69. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  70. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +73 -0
  71. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +86 -0
  72. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +35 -48
  73. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +115 -29
  74. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +39 -144
  75. package/Components/MasterCell/elementMapper.tsx +1 -0
  76. package/Components/MasterCell/hoc/__tests__/withAsyncRender.test.tsx +219 -0
  77. package/Components/MasterCell/hoc/withAsyncRender.tsx +9 -7
  78. package/Components/MasterCell/index.tsx +2 -0
  79. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +82 -3
  80. package/Components/MasterCell/utils/index.ts +61 -31
  81. package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +102 -87
  82. package/Components/MeasurmentsPortal/__tests__/MeasurementsPortal.test.tsx +355 -0
  83. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +17 -9
  84. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +16 -8
  85. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  86. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  87. package/Components/OfflineHandler/NotificationView/utils.ts +34 -0
  88. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  89. package/Components/PlayerContainer/PlayerContainer.tsx +43 -64
  90. package/Components/PlayerImageBackground/index.tsx +3 -22
  91. package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
  92. package/Components/PreloaderWrapper/index.tsx +15 -0
  93. package/Components/River/ComponentsMap/ComponentsMap.tsx +18 -16
  94. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  95. package/Components/River/RefreshControl.tsx +19 -82
  96. package/Components/River/River.tsx +9 -82
  97. package/Components/River/RiverItem.tsx +26 -20
  98. package/Components/River/TV/River.tsx +31 -14
  99. package/Components/River/TV/index.tsx +8 -4
  100. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  101. package/Components/River/TV/utils/index.ts +4 -0
  102. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  103. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  104. package/Components/River/__tests__/componentsMap.test.js +38 -0
  105. package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
  106. package/Components/River/hooks/index.ts +1 -0
  107. package/Components/River/hooks/usePullToRefresh.ts +51 -0
  108. package/Components/Screen/TV/index.web.tsx +4 -2
  109. package/Components/Screen/__tests__/Screen.test.tsx +66 -42
  110. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  111. package/Components/Screen/hooks.ts +75 -6
  112. package/Components/Screen/index.tsx +9 -4
  113. package/Components/Screen/navigationHandler.ts +49 -24
  114. package/Components/Screen/orientationHandler.ts +10 -13
  115. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
  116. package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
  117. package/Components/ScreenFeedLoader/index.ts +1 -0
  118. package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
  119. package/Components/ScreenResolver/hooks/index.ts +3 -0
  120. package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
  121. package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
  122. package/Components/ScreenResolver/index.tsx +15 -111
  123. package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
  124. package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
  125. package/Components/ScreenResolver/utils/index.ts +1 -0
  126. package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
  127. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
  128. package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
  129. package/Components/ScreenResolverFeedProvider/index.ts +1 -0
  130. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  131. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  132. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +44 -26
  133. package/Components/Tabs/TV/Tabs.tsx +20 -3
  134. package/Components/Tabs/TabContent.tsx +7 -4
  135. package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
  136. package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
  137. package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
  138. package/Components/TopCutoffOverlay/index.tsx +55 -0
  139. package/Components/Transitioner/Scene.tsx +10 -3
  140. package/Components/Transitioner/index.js +3 -3
  141. package/Components/VideoLive/LiveImageManager.ts +199 -54
  142. package/Components/VideoLive/PlayerLiveImageComponent.tsx +31 -33
  143. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
  144. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +1 -0
  145. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +118 -171
  146. package/Components/VideoModal/ModalAnimation/index.ts +2 -13
  147. package/Components/VideoModal/ModalAnimation/utils.ts +1 -327
  148. package/Components/VideoModal/PlayerWrapper.tsx +14 -88
  149. package/Components/VideoModal/VideoModal.tsx +1 -5
  150. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  151. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  152. package/Components/VideoModal/hooks/useModalSize.ts +10 -5
  153. package/Components/VideoModal/playerWrapperStyle.ts +70 -0
  154. package/Components/VideoModal/playerWrapperUtils.ts +91 -0
  155. package/Components/VideoModal/utils.ts +19 -9
  156. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
  157. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  158. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  159. package/Components/ZappUIComponent/index.tsx +12 -6
  160. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  161. package/Components/index.js +1 -1
  162. package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
  163. package/Contexts/ScreenContext/index.tsx +71 -19
  164. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  165. package/Contexts/ZappHookModalContext/index.tsx +37 -61
  166. package/Contexts/ZappPipesContext/ZappPipesContextFactory.tsx +18 -7
  167. package/Contexts/index.ts +0 -2
  168. package/Decorators/Analytics/index.tsx +6 -5
  169. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  170. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  171. package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -7
  172. package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +212 -5
  173. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
  174. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  175. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  176. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  177. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
  178. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  179. package/Helpers/DataSourceHelper/index.ts +19 -0
  180. package/events/index.ts +3 -0
  181. package/events/scrollEndReached.ts +15 -0
  182. package/index.d.ts +7 -0
  183. package/package.json +6 -5
  184. package/Components/MasterCell/DefaultComponents/Text/utils/__tests__/withAdjustedLineHeight.test.ts +0 -46
  185. package/Components/MasterCell/DefaultComponents/Text/utils/index.ts +0 -21
  186. package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
  187. package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
  188. package/Components/River/TV/withTVEventHandler.tsx +0 -27
  189. package/Components/VideoModal/ModalAnimation/AnimatedPlayerModalWrapper.tsx +0 -60
  190. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +0 -417
  191. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.web.tsx +0 -294
  192. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +0 -176
  193. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.web.tsx +0 -93
  194. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +0 -500
  195. package/Components/VideoModal/ModalAnimation/__tests__/getMoveUpValue.test.ts +0 -108
  196. package/Helpers/DataSourceHelper/index.js +0 -19
  197. /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
@@ -4,9 +4,14 @@ import {
4
4
  playerManager,
5
5
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
6
6
  import { setUserCellPlayerMutedPreference } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference";
7
+ import { playerFactory } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerFactory";
8
+ import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/conts";
7
9
  import { loggerLiveImageManager } from "./loggerHelper";
8
10
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
9
- import { Component } from "react";
11
+ import {
12
+ executePreloadHooks,
13
+ PreloadHookConfig,
14
+ } from "../MasterCell/DefaultComponents/LiveImage/executePreloadHooks";
10
15
 
11
16
  const TIMEOUT_FOR_DELAY_CHECK_PLAYER_POSITION = 500; // ms
12
17
 
@@ -40,13 +45,21 @@ type Position = {
40
45
  right: number;
41
46
  };
42
47
 
48
+ type PlayerFactoryConfig = {
49
+ player: any; // React ref for native view
50
+ playerId: string;
51
+ muted: boolean;
52
+ playerPluginId: string;
53
+ screenConfig: Record<string, any>;
54
+ entry: ZappEntry; // original entry, used as fallback if no hooks
55
+ };
56
+
43
57
  type LiveImageProps = {
44
- player: Player;
45
58
  playerId: string;
46
59
  setMode?: (type: LiveImageType) => void;
47
- component: Component;
48
- // TODO: ...primary, powerCell, tvGallery, etc.
49
- // type: string;
60
+ preloadHooks?: PreloadHookConfig[];
61
+ factoryConfig: PlayerFactoryConfig;
62
+ tag: string;
50
63
  };
51
64
 
52
65
  // Disabled because we have only unmute button but no play/pause state anymore
@@ -85,7 +98,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
85
98
 
86
99
  public register = (item: LiveImage): (() => void) => {
87
100
  this.items.push(item);
88
- log_debug(`register: live image ${playerInfo(item.player)}`);
101
+ log_debug(`register: live image ${item.playerId} - ${item.tag}`);
89
102
 
90
103
  // TV only Start playing video once registered
91
104
  if (isTV()) {
@@ -96,15 +109,13 @@ export class LiveImageManager implements PlayerLifecycleListener {
96
109
  };
97
110
 
98
111
  public unregister = (item: LiveImage) => {
99
- log_debug(`unregister: live-image ${playerInfo(item.player)}`);
112
+ log_debug(`unregister: live-image ${item.playerId} - ${item.tag}`);
100
113
 
101
114
  if (this.currentlyPlaying === item) {
102
115
  this.currentlyPlaying = null;
103
116
 
104
117
  log_debug(
105
- `unregister: currently playing live-image was destroyed, ${playerInfo(
106
- item.player
107
- )}`
118
+ `unregister: currently playing live-image was destroyed, ${item.playerId} - ${item.tag}`
108
119
  );
109
120
 
110
121
  // TODO: Maybe start another one
@@ -118,7 +129,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
118
129
  item,
119
130
  playerId: this.currentlyPlaying?.playerId,
120
131
  primaryPlayerId: this.primaryPlayer?.playerId,
121
- entry: item.getPlayer().getEntry(),
132
+ entry: item.getPlayer()?.getEntry(),
122
133
  },
123
134
  });
124
135
  };
@@ -130,9 +141,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
130
141
 
131
142
  public onViewportEnter = (item: LiveImage) => {
132
143
  log_debug(
133
- `onViewportEnter: live-image ${playerInfo(
134
- item.player
135
- )}, primary ${playerInfo(this.primaryPlayer)}`
144
+ `onViewportEnter: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
136
145
  );
137
146
 
138
147
  if (!isTV()) {
@@ -154,9 +163,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
154
163
 
155
164
  public onViewportLeave = (item: LiveImage) => {
156
165
  log_debug(
157
- `onViewportLeave: live-image playerId: ${playerInfo(
158
- item.player
159
- )}, primary ${playerInfo(this.primaryPlayer)}`
166
+ `onViewportLeave: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
160
167
  );
161
168
 
162
169
  this.pauseItem(item);
@@ -190,20 +197,29 @@ export class LiveImageManager implements PlayerLifecycleListener {
190
197
  this.items.find((i) => i.playerId === playerId) || null;
191
198
 
192
199
  private pauseItem = (item: LiveImage) => {
193
- log_debug(`pauseItem: live-image ${playerInfo(item.player)}`);
200
+ log_debug(`pauseItem: live-image ${item.playerId} - ${item.tag}`);
201
+
202
+ if (!item.player) {
203
+ // Player not yet created (e.g. hooks still running) — just reset mode
204
+ item.setMode?.(LiveImageType.Image);
205
+
206
+ if (item === this.currentlyPlaying) {
207
+ this.currentlyPlaying = null;
208
+ }
209
+
210
+ return;
211
+ }
194
212
 
195
213
  if (!item.player.playerState.isReadyToPlay) {
196
214
  log_debug(
197
- `playItem: live-image not ready, will start playback after loading, ${playerInfo(
198
- item.player
199
- )}`
215
+ `pauseItem: live-image not ready, ${item.playerId} - ${item.tag}`
200
216
  );
201
217
  } else {
202
- item.player?.pause();
218
+ item.player.pause();
203
219
  }
204
220
 
205
221
  // Fake close event, because we unmount native view
206
- item.player?.onPlayerClose();
222
+ item.player.onPlayerClose();
207
223
  item.setMode?.(LiveImageType.Image);
208
224
 
209
225
  if (item === this.currentlyPlaying) {
@@ -213,9 +229,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
213
229
 
214
230
  public playLiveImage = (item: LiveImage) => {
215
231
  log_debug(
216
- `playLiveImage: live-image ${playerInfo(
217
- item.player
218
- )}, primary ${playerInfo(this.primaryPlayer)}`
232
+ `playLiveImage: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
219
233
  );
220
234
 
221
235
  if (this.primaryPlayer) {
@@ -223,7 +237,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
223
237
  }
224
238
 
225
239
  if (this.currentlyPlaying) {
226
- if (this.currentlyPlaying?.player?.playerId === item.player.playerId) {
240
+ if (this.currentlyPlaying.playerId === item.playerId) {
227
241
  return;
228
242
  } else {
229
243
  this.pauseItem(this.currentlyPlaying);
@@ -231,18 +245,42 @@ export class LiveImageManager implements PlayerLifecycleListener {
231
245
  }
232
246
 
233
247
  this.currentlyPlaying = item;
234
- item.setMode?.(LiveImageType.Video);
235
248
 
236
- if (item.player.playerState.isReadyToPlay) {
237
- item.player.play();
238
- }
249
+ item
250
+ .prepareForPlayback()
251
+ .then((result) => {
252
+ if (!result) {
253
+ log_error(
254
+ `Failed to prepare live image ${item.playerId} - ${item.tag} for playback: prepareForPlayback returned false`
255
+ );
256
+
257
+ this.currentlyPlaying = null;
258
+ item.setMode?.(LiveImageType.Image);
259
+
260
+ return;
261
+ }
262
+
263
+ // Guard: item might have been replaced while hooks were running
264
+ if (this.currentlyPlaying !== item) return;
265
+
266
+ item.setMode?.(LiveImageType.Video);
267
+
268
+ if (item.player?.playerState.isReadyToPlay) {
269
+ item.player.play();
270
+ }
271
+ })
272
+ .catch((error) => {
273
+ log_error(
274
+ `Failed to prepare live image ${item.playerId} - ${item.tag} for playback: ${error?.message}`
275
+ );
276
+
277
+ this.onLiveImageError(item, error);
278
+ });
239
279
  };
240
280
 
241
281
  public pauseLiveImage = (item: LiveImage) => {
242
282
  log_debug(
243
- `pauseLiveImage: live-image playerId: ${playerInfo(
244
- item.player
245
- )}, primary ${playerInfo(this.primaryPlayer)}`
283
+ `pauseLiveImage: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
246
284
  );
247
285
 
248
286
  this.pauseItem(item);
@@ -258,7 +296,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
258
296
 
259
297
  setUserCellPlayerMutedPreference(true);
260
298
 
261
- this.items.forEach((liveImage) => liveImage.player.mute());
299
+ this.items.forEach((liveImage) => liveImage.player?.mute());
262
300
  };
263
301
 
264
302
  public unmuteAll = () => {
@@ -266,16 +304,36 @@ export class LiveImageManager implements PlayerLifecycleListener {
266
304
 
267
305
  setUserCellPlayerMutedPreference(false);
268
306
 
269
- this.items.forEach((liveImage) => liveImage.player.unmute());
307
+ this.items.forEach((liveImage) => liveImage.player?.unmute());
270
308
  };
271
309
 
272
- public checkPlayerPosition = (item: LiveImage) => {
310
+ public onViewPositionChanged = (item: LiveImage) => {
311
+ log_debug(
312
+ `onViewPositionChanged: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
313
+ );
314
+
315
+ if (!isTV()) {
316
+ // mobile only
317
+ // we have to delay running checkPlayerPosition, because sometimes on fast scrolling we get wrong order onEnter, then onLeave.
318
+ // which could cause select wrong item to play
319
+
320
+ this.cancelCheckPlayerPositionTimeout();
321
+
322
+ this.checkPlayerPositionTimeout = setTimeout(() => {
323
+ this.cancelCheckPlayerPositionTimeout();
324
+
325
+ this.checkPlayerPosition(item);
326
+ }, TIMEOUT_FOR_DELAY_CHECK_PLAYER_POSITION);
327
+ } else {
328
+ this.checkPlayerPosition(item);
329
+ }
330
+ };
331
+
332
+ private checkPlayerPosition = (item: LiveImage) => {
273
333
  this.cancelCheckPlayerPositionTimeout();
274
334
 
275
335
  log_debug(
276
- `checkPlayerPosition: live-image playerId: ${playerInfo(
277
- item.player
278
- )}, primary ${playerInfo(this.primaryPlayer)}`
336
+ `checkPlayerPosition: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
279
337
  );
280
338
 
281
339
  const playerItem = this.findNextPlayableItem();
@@ -391,7 +449,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
391
449
  item,
392
450
  playerId: this.currentlyPlaying?.playerId,
393
451
  primaryPlayerId: this.primaryPlayer?.playerId,
394
- entry: item.getPlayer().getEntry(),
452
+ entry: item.getPlayer()?.getEntry(),
395
453
  },
396
454
  });
397
455
  };
@@ -426,7 +484,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
426
484
  item,
427
485
  playerId: this.currentlyPlaying?.playerId,
428
486
  primaryPlayerId: this.primaryPlayer?.playerId,
429
- entry: item.getPlayer().getEntry(),
487
+ entry: item.getPlayer()?.getEntry(),
430
488
  },
431
489
  });
432
490
  };
@@ -448,7 +506,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
448
506
  this.currentlyPlaying = null;
449
507
 
450
508
  log_debug(
451
- `onLiveImageError: currentitem: ${currentItem.playerId} was removed`
509
+ `onLiveImageError: currentItem: ${currentItem.playerId} - ${currentItem.tag} was removed`
452
510
  );
453
511
 
454
512
  // TODO: ...Maybe player some other item
@@ -460,8 +518,8 @@ export class LiveImageManager implements PlayerLifecycleListener {
460
518
  item,
461
519
  error,
462
520
  playerId: currentItem?.playerId,
463
- primaryPlayerId: currentItem?.playerId,
464
- entry: item.getPlayer().getEntry(),
521
+ primaryPlayerId: this.primaryPlayer?.playerId,
522
+ entry: item.getPlayer()?.getEntry(),
465
523
  },
466
524
  });
467
525
  };
@@ -503,10 +561,10 @@ export class LiveImageManager implements PlayerLifecycleListener {
503
561
  LiveImageManager.instance;
504
562
 
505
563
  export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
506
- public player: Player;
507
- public setMode: (type: LiveImageType) => void;
508
- // Will be replaced with rects
564
+ public player: Player | null = null;
565
+ public setMode?: (type: LiveImageType) => void;
509
566
  public isFullyVisible: boolean = false;
567
+ public tag: string;
510
568
  public position: Position = {
511
569
  centerX: 0,
512
570
  centerY: 0,
@@ -516,18 +574,105 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
516
574
  left: 0,
517
575
  };
518
576
 
577
+ positionToString() {
578
+ if (!this.position) {
579
+ return "position not set";
580
+ }
581
+
582
+ const { centerX, centerY, top, bottom, left, right } = this.position;
583
+
584
+ return `centerX: ${centerX.toFixed(2)}, centerY: ${centerY.toFixed(2)}, top: ${top.toFixed(2)}, bottom: ${bottom.toFixed(2)}, left: ${left.toFixed(2)}, right: ${right.toFixed(2)}`;
585
+ }
586
+
519
587
  readonly playerId: string;
520
- readonly component: Component;
588
+ public component: any = null;
589
+
590
+ private factoryConfig: PlayerFactoryConfig;
591
+ public preloadHooks?: PreloadHookConfig[];
592
+ public processedEntry: ZappEntry | null = null;
593
+ private _preparePromise: Promise<boolean> | null = null;
521
594
 
522
595
  constructor(props: LiveImageProps) {
523
- this.player = props.player;
524
596
  this.setMode = props.setMode;
525
- this.playerId = this.player.playerId;
526
- this.component = props.component;
527
- this.player.addListener({ id: "live-image", listener: this });
597
+ this.playerId = props.playerId;
598
+ this.preloadHooks = props.preloadHooks;
599
+ this.factoryConfig = props.factoryConfig;
600
+ this.tag = props.tag || "untagged";
601
+ }
602
+
603
+ async prepareForPlayback(): Promise<boolean> {
604
+ // Already prepared — player exists
605
+ if (this.player) {
606
+ return true;
607
+ }
608
+
609
+ // Deduplicate: if preparation is already in flight, await the same promise
610
+ if (this._preparePromise) {
611
+ return this._preparePromise;
612
+ }
613
+
614
+ this._preparePromise = (async (): Promise<boolean> => {
615
+ // 1. Run hooks if configured
616
+ let entry = this.factoryConfig.entry;
617
+
618
+ if (this.preloadHooks?.length) {
619
+ const result = await executePreloadHooks({
620
+ preloadHooks: this.preloadHooks,
621
+ entry,
622
+ });
623
+
624
+ if (result) {
625
+ this.processedEntry = result;
626
+ entry = result;
627
+ } else {
628
+ return false;
629
+ }
630
+ }
631
+
632
+ // 2. Create the player with the correct entry
633
+ const factoryItem = playerFactory({
634
+ player: this.factoryConfig.player,
635
+ playerId: this.factoryConfig.playerId,
636
+ autoplay: false,
637
+ entry,
638
+ muted: this.factoryConfig.muted,
639
+ playerPluginId: this.factoryConfig.playerPluginId,
640
+ screenConfig: this.factoryConfig.screenConfig,
641
+ playerRole: PlayerRole.Cell,
642
+ });
643
+
644
+ if (!factoryItem) {
645
+ throw new Error("Player factory returned null");
646
+ }
647
+
648
+ this.player = factoryItem.controller;
649
+ this.component = factoryItem.Component;
650
+
651
+ // 3. Register callbacks — player now exists
652
+ this.player.addListener({ id: "live-image", listener: this });
653
+
654
+ return true;
655
+ })()
656
+ .then((result) => {
657
+ this._preparePromise = null;
658
+
659
+ return result;
660
+ })
661
+ .catch((error) => {
662
+ this._preparePromise = null;
663
+
664
+ log_error(
665
+ `prepareForPlayback: live-image ${this.playerId}, error preparing for playback: ${error?.message}`,
666
+ { error }
667
+ );
668
+
669
+ throw error;
670
+ });
671
+
672
+ return this._preparePromise;
528
673
  }
529
674
 
530
- public getPlayer = (): Player => {
675
+ public getPlayer = (): Player | null => {
531
676
  return this.player;
532
677
  };
533
678
 
@@ -7,8 +7,6 @@ import { AppState, AppStateStatus, View } from "react-native";
7
7
  import { TrackedView } from "../TrackedView";
8
8
  import { useDimensions } from "@applicaster/zapp-react-native-utils/reactHooks/layout";
9
9
 
10
- import { playerFactory } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerFactory";
11
-
12
10
  import {
13
11
  isApplePlatform,
14
12
  isTV,
@@ -22,15 +20,15 @@ import { AnimatedInOut } from "@applicaster/zapp-react-native-ui-components/Comp
22
20
  import { overlayFadeIn } from "./animationUtils";
23
21
  import { loggerLiveImageComponent } from "./loggerHelper";
24
22
  import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
25
- import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/conts";
26
23
  import { getAutoplaySettings } from "./utils";
27
24
  import { isString } from "@applicaster/zapp-react-native-utils/stringUtils";
28
25
  import { BufferAnimation } from "../PlayerContainer/BufferAnimation";
26
+ import { PreloadHookConfig } from "../MasterCell/DefaultComponents/LiveImage/executePreloadHooks";
29
27
 
30
28
  const { log_error, log_debug } = loggerLiveImageComponent;
31
29
 
32
- const isMeasurement = (item: ZappEntry) =>
33
- isString(item.id) && item.id.startsWith("pre-measurement-");
30
+ const isMeasurement = (itemId: string | number | null): boolean =>
31
+ isString(itemId) && itemId.startsWith("pre-measurement-");
34
32
 
35
33
  // Pixels by which the view can slightly extend outside the viewport and still be considered fully visible.
36
34
  const CLIP_THRESHOLD = 10;
@@ -44,6 +42,7 @@ type Props = {
44
42
  screenConfig: Record<string, any>;
45
43
  audioMutedByDefault?: boolean;
46
44
  uri: string; // cover image url
45
+ preloadHooks?: PreloadHookConfig[];
47
46
  };
48
47
 
49
48
  const PlayerLiveImageComponent = (props: Props) => {
@@ -116,43 +115,36 @@ const PlayerLiveImageComponent = (props: Props) => {
116
115
  }
117
116
 
118
117
  entryToViewIdMapping.current[nativeTag] = { itemId: item.id };
119
- }, [trackViewRef.current, item.id, mode]);
118
+ }, [item.id, mode]);
120
119
 
121
- const { screenConfig, playerPluginId } = props;
120
+ const { screenConfig, playerPluginId, preloadHooks } = props;
122
121
 
123
122
  const liveImageItem: LiveImage = React.useMemo(() => {
124
- const playerFactoryItem = playerFactory({
125
- player: ref,
123
+ return new LiveImage({
126
124
  playerId,
127
- autoplay: false,
128
- entry: item,
129
- muted, // Initial muted state, not needed in dependencies
130
- playerPluginId: playerPluginId,
131
- screenConfig: screenConfig,
132
- playerRole: PlayerRole.Cell,
133
- });
134
-
135
- if (playerFactoryItem) {
136
- return new LiveImage({
125
+ preloadHooks,
126
+ factoryConfig: {
127
+ player: ref,
137
128
  playerId,
138
- player: playerFactoryItem.controller,
139
- component: playerFactoryItem?.Component,
140
- });
141
- } else {
142
- throw new Error("Player factory item is null");
143
- }
144
- }, [playerId, item.id, playerPluginId, screenConfig]);
129
+ muted,
130
+ playerPluginId: playerPluginId,
131
+ screenConfig: screenConfig,
132
+ entry: item,
133
+ },
134
+ tag: item.title?.toString(),
135
+ });
136
+ }, [playerId, preloadHooks, muted, playerPluginId, screenConfig, item]);
145
137
 
146
138
  React.useEffect(() => {
147
139
  liveImageItem.setMode = setModeDebounced;
148
140
  }, [setModeDebounced, liveImageItem]);
149
141
 
142
+ // todo: no need for it to be react, can be moved into `player.addListener`
150
143
  const { start, end } = React.useMemo(
151
144
  () => getAutoplaySettings(item),
152
145
  [item.id]
153
146
  );
154
147
 
155
- const controller = liveImageItem.getPlayer();
156
148
  const player = usePlayer(playerId);
157
149
 
158
150
  const _assignRoot = (component) => {
@@ -169,7 +161,7 @@ const PlayerLiveImageComponent = (props: Props) => {
169
161
 
170
162
  React.useEffect(() => {
171
163
  // FIXME - find a more elegant way to disable live-image on cell for measurement
172
- if (isMeasurement(item)) {
164
+ if (isMeasurement(item.id)) {
173
165
  return;
174
166
  }
175
167
 
@@ -253,7 +245,13 @@ const PlayerLiveImageComponent = (props: Props) => {
253
245
  }, [item.id]);
254
246
 
255
247
  React.useEffect(() => {
256
- if (isMeasurement(item) || !playerManager) {
248
+ if (isMeasurement(item.id) || !playerManager) {
249
+ return;
250
+ }
251
+
252
+ const controller = liveImageItem.getPlayer();
253
+
254
+ if (!controller) {
257
255
  return;
258
256
  }
259
257
 
@@ -273,7 +271,7 @@ const PlayerLiveImageComponent = (props: Props) => {
273
271
  playerManager.unregisterPlayer(playerId);
274
272
  };
275
273
  }
276
- }, [liveImageItem, playerId, controller, item.id]);
274
+ }, [liveImageItem, playerId, mode, item.id]);
277
275
 
278
276
  const onPositionUpdated = React.useCallback(
279
277
  (data) => {
@@ -310,7 +308,7 @@ const PlayerLiveImageComponent = (props: Props) => {
310
308
  ? LiveImageManager.instance.onViewportEnter(liveImageItem)
311
309
  : LiveImageManager.instance.onViewportLeave(liveImageItem);
312
310
  } else {
313
- LiveImageManager.instance.checkPlayerPosition(liveImageItem);
311
+ LiveImageManager.instance.onViewPositionChanged(liveImageItem);
314
312
  }
315
313
  },
316
314
  [liveImageItem, dimensions.height, dimensions.width]
@@ -338,11 +336,11 @@ const PlayerLiveImageComponent = (props: Props) => {
338
336
  <Player
339
337
  autoplay={false}
340
338
  ref={_assignRoot}
341
- entry={item} // Must be passed first in list
339
+ entry={liveImageItem.processedEntry || item} // Must be passed first in list
342
340
  style={videoStyles}
343
341
  playerId={playerId}
344
342
  muted={muted}
345
- listener={controller?.getListener()}
343
+ listener={liveImageItem.getPlayer()?.getListener()}
346
344
  resizeMode={"cover"}
347
345
  {...platformSpecificProps}
348
346
  />
@@ -120,29 +120,14 @@ describe("PlayerLiveImageComponent", () => {
120
120
  // TODO: implement this test
121
121
  });
122
122
 
123
- it("should register the player for normal item", () => {
123
+ it("should not register the player at mount (lazy creation)", () => {
124
124
  render(
125
125
  <Wrapper>
126
126
  <PlayerLiveImage {...defaultProps} />
127
127
  </Wrapper>
128
128
  );
129
129
 
130
- const isPlayerRegistered = playerManager.isPlayerRegistered(
131
- defaultProps.playerId
132
- );
133
-
134
- expect(isPlayerRegistered).toBe(true);
135
- });
136
-
137
- it("should unregister the player for normal item", () => {
138
- const component = render(
139
- <Wrapper>
140
- <PlayerLiveImage {...defaultProps} />
141
- </Wrapper>
142
- );
143
-
144
- component.unmount();
145
-
130
+ // Player is not registered until playback starts (lazy creation)
146
131
  const isPlayerRegistered = playerManager.isPlayerRegistered(
147
132
  defaultProps.playerId
148
133
  );
@@ -9,6 +9,7 @@ exports[`PlayerLiveImageComponent should render correctly with default props 1`]
9
9
  <View>
10
10
  <View
11
11
  collapsable={false}
12
+ renderToHardwareTextureAndroid={false}
12
13
  style={
13
14
  {
14
15
  "opacity": 1,