@applicaster/zapp-react-native-ui-components 15.0.0-rc.99 → 15.1.0-alpha.1852552014

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 (137) hide show
  1. package/Components/BackgroundImage/BackgroundImage.tv.android.tsx +28 -0
  2. package/Components/BackgroundImage/BackgroundImage.tv.ios.tsx +24 -0
  3. package/Components/BackgroundImage/BackgroundImage.tv.web.tsx +42 -0
  4. package/Components/BackgroundImage/index.ts +8 -0
  5. package/Components/BaseFocusable/index.ios.ts +2 -12
  6. package/Components/Cell/FocusableWrapper.tsx +0 -3
  7. package/Components/Cell/TvOSCellComponent.tsx +0 -5
  8. package/Components/CellRendererResolver/index.ts +1 -1
  9. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  10. package/Components/Focusable/Focusable.tsx +2 -4
  11. package/Components/Focusable/FocusableTvOS.tsx +1 -18
  12. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +0 -1
  13. package/Components/FocusableGroup/FocusableTvOS.tsx +12 -37
  14. package/Components/GeneralContentScreen/GeneralContentScreen.tsx +39 -28
  15. package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
  16. package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
  17. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  18. package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
  19. package/Components/HandlePlayable/HandlePlayable.tsx +24 -42
  20. package/Components/HandlePlayable/utils.ts +31 -0
  21. package/Components/HookRenderer/HookRenderer.tsx +40 -10
  22. package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
  23. package/Components/Layout/FullWidthRow.tsx +38 -0
  24. package/Components/Layout/TV/ScreenContainer.tsx +11 -2
  25. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +5 -1
  26. package/Components/Layout/TV/index.tsx +6 -6
  27. package/Components/Layout/TV/index.web.tsx +6 -6
  28. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  29. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +10 -4
  30. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +1 -5
  31. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +3 -11
  32. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +1 -9
  33. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +14 -15
  34. package/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
  35. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +136 -0
  36. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +34 -16
  37. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +6 -7
  38. package/Components/MasterCell/DefaultComponents/Text/index.tsx +2 -6
  39. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +2 -6
  40. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +11 -233
  41. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +15 -19
  42. package/Components/MasterCell/dataAdapter.ts +15 -5
  43. package/Components/MasterCell/index.tsx +10 -1
  44. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  45. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  46. package/Components/MasterCell/utils/index.ts +89 -46
  47. package/Components/Navigator/StackNavigator.tsx +6 -0
  48. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  49. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +18 -17
  50. package/Components/OfflineHandler/__tests__/index.test.tsx +18 -27
  51. package/Components/PlayerContainer/PlayerContainer.tsx +20 -34
  52. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +286 -0
  53. package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
  54. package/Components/PreloaderWrapper/index.tsx +15 -0
  55. package/Components/River/ComponentsMap/ComponentsMap.tsx +3 -4
  56. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  57. package/Components/River/RefreshControl.tsx +9 -3
  58. package/Components/River/RiverItem.tsx +26 -20
  59. package/Components/River/TV/River.tsx +14 -31
  60. package/Components/River/TV/index.tsx +4 -8
  61. package/Components/River/TV/withTVEventHandler.tsx +36 -0
  62. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +0 -16
  63. package/Components/River/__tests__/componentsMap.test.js +0 -38
  64. package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
  65. package/Components/Screen/TV/hooks/index.ts +2 -0
  66. package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
  67. package/Components/Screen/TV/index.web.tsx +18 -11
  68. package/Components/Screen/__tests__/Screen.test.tsx +43 -65
  69. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +44 -68
  70. package/Components/Screen/hooks.ts +76 -5
  71. package/Components/Screen/index.tsx +18 -7
  72. package/Components/Screen/orientationHandler.ts +3 -3
  73. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
  74. package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
  75. package/Components/ScreenFeedLoader/index.ts +1 -0
  76. package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
  77. package/Components/ScreenResolver/hooks/index.ts +3 -0
  78. package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
  79. package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
  80. package/Components/ScreenResolver/index.tsx +9 -115
  81. package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
  82. package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
  83. package/Components/ScreenResolver/utils/index.ts +1 -0
  84. package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
  85. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
  86. package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
  87. package/Components/ScreenResolverFeedProvider/index.ts +1 -0
  88. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  89. package/Components/ScreenRevealManager/ScreenRevealManager.ts +8 -40
  90. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  91. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +69 -86
  92. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +10 -18
  93. package/Components/Tabs/TabContent.tsx +4 -7
  94. package/Components/Transitioner/Scene.tsx +9 -15
  95. package/Components/Transitioner/index.js +3 -3
  96. package/Components/VideoLive/LiveImageManager.ts +209 -53
  97. package/Components/VideoLive/PlayerLiveImageComponent.tsx +32 -32
  98. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
  99. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +5 -5
  100. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +7 -15
  101. package/Components/VideoModal/utils.ts +16 -13
  102. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  103. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  104. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  105. package/Components/ZappUIComponent/index.tsx +12 -6
  106. package/Components/index.js +1 -1
  107. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  108. package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
  109. package/Contexts/ScreenContext/index.tsx +64 -26
  110. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  111. package/Contexts/ZappPipesContext/ZappPipesContextFactory.tsx +18 -7
  112. package/Decorators/Analytics/index.tsx +5 -6
  113. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +0 -1
  114. package/Decorators/ConfigurationWrapper/const.ts +0 -1
  115. package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -7
  116. package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +212 -5
  117. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  118. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  119. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  120. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  121. package/Helpers/DataSourceHelper/index.js +19 -0
  122. package/Helpers/index.js +7 -40
  123. package/package.json +5 -5
  124. package/Components/Layout/TV/LayoutBackground.tsx +0 -31
  125. package/Components/MasterCell/DefaultComponents/Text/utils/__tests__/withAdjustedLineHeight.test.ts +0 -46
  126. package/Components/MasterCell/DefaultComponents/Text/utils/index.ts +0 -21
  127. package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
  128. package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
  129. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +0 -101
  130. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +0 -30
  131. package/Components/River/TV/utils/index.ts +0 -4
  132. package/Components/River/TV/withFocusableGroupForContent.tsx +0 -71
  133. package/Components/Screen/utils.ts +0 -16
  134. package/Helpers/Analytics/index.js +0 -95
  135. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +0 -80
  136. package/Helpers/DataSourceHelper/index.ts +0 -19
  137. /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,20 @@ 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
+ playerPluginId: string;
52
+ screenConfig: Record<string, any>;
53
+ entry: ZappEntry; // original entry, used as fallback if no hooks
54
+ };
55
+
43
56
  type LiveImageProps = {
44
- player: Player;
45
57
  playerId: string;
46
58
  setMode?: (type: LiveImageType) => void;
47
- component: Component;
48
- // TODO: ...primary, powerCell, tvGallery, etc.
49
- // type: string;
59
+ preloadHooks?: PreloadHookConfig[];
60
+ factoryConfig: PlayerFactoryConfig;
61
+ tag: string;
50
62
  };
51
63
 
52
64
  // Disabled because we have only unmute button but no play/pause state anymore
@@ -85,7 +97,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
85
97
 
86
98
  public register = (item: LiveImage): (() => void) => {
87
99
  this.items.push(item);
88
- log_debug(`register: live image ${playerInfo(item.player)}`);
100
+ log_debug(`register: live image ${item.playerId} - ${item.tag}`);
89
101
 
90
102
  // TV only Start playing video once registered
91
103
  if (isTV()) {
@@ -96,15 +108,13 @@ export class LiveImageManager implements PlayerLifecycleListener {
96
108
  };
97
109
 
98
110
  public unregister = (item: LiveImage) => {
99
- log_debug(`unregister: live-image ${playerInfo(item.player)}`);
111
+ log_debug(`unregister: live-image ${item.playerId} - ${item.tag}`);
100
112
 
101
113
  if (this.currentlyPlaying === item) {
102
114
  this.currentlyPlaying = null;
103
115
 
104
116
  log_debug(
105
- `unregister: currently playing live-image was destroyed, ${playerInfo(
106
- item.player
107
- )}`
117
+ `unregister: currently playing live-image was destroyed, ${item.playerId} - ${item.tag}`
108
118
  );
109
119
 
110
120
  // TODO: Maybe start another one
@@ -118,7 +128,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
118
128
  item,
119
129
  playerId: this.currentlyPlaying?.playerId,
120
130
  primaryPlayerId: this.primaryPlayer?.playerId,
121
- entry: item.getPlayer().getEntry(),
131
+ entry: item.getPlayer()?.getEntry(),
122
132
  },
123
133
  });
124
134
  };
@@ -130,9 +140,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
130
140
 
131
141
  public onViewportEnter = (item: LiveImage) => {
132
142
  log_debug(
133
- `onViewportEnter: live-image ${playerInfo(
134
- item.player
135
- )}, primary ${playerInfo(this.primaryPlayer)}`
143
+ `onViewportEnter: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
136
144
  );
137
145
 
138
146
  if (!isTV()) {
@@ -154,9 +162,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
154
162
 
155
163
  public onViewportLeave = (item: LiveImage) => {
156
164
  log_debug(
157
- `onViewportLeave: live-image playerId: ${playerInfo(
158
- item.player
159
- )}, primary ${playerInfo(this.primaryPlayer)}`
165
+ `onViewportLeave: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
160
166
  );
161
167
 
162
168
  this.pauseItem(item);
@@ -190,20 +196,29 @@ export class LiveImageManager implements PlayerLifecycleListener {
190
196
  this.items.find((i) => i.playerId === playerId) || null;
191
197
 
192
198
  private pauseItem = (item: LiveImage) => {
193
- log_debug(`pauseItem: live-image ${playerInfo(item.player)}`);
199
+ log_debug(`pauseItem: live-image ${item.playerId} - ${item.tag}`);
200
+
201
+ if (!item.player) {
202
+ // Player not yet created (e.g. hooks still running) — just reset mode
203
+ item.setMode?.(LiveImageType.Image);
204
+
205
+ if (item === this.currentlyPlaying) {
206
+ this.currentlyPlaying = null;
207
+ }
208
+
209
+ return;
210
+ }
194
211
 
195
212
  if (!item.player.playerState.isReadyToPlay) {
196
213
  log_debug(
197
- `playItem: live-image not ready, will start playback after loading, ${playerInfo(
198
- item.player
199
- )}`
214
+ `pauseItem: live-image not ready, ${item.playerId} - ${item.tag}`
200
215
  );
201
216
  } else {
202
- item.player?.pause();
217
+ item.player.pause();
203
218
  }
204
219
 
205
220
  // Fake close event, because we unmount native view
206
- item.player?.onPlayerClose();
221
+ item.player.onPlayerClose();
207
222
  item.setMode?.(LiveImageType.Image);
208
223
 
209
224
  if (item === this.currentlyPlaying) {
@@ -213,9 +228,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
213
228
 
214
229
  public playLiveImage = (item: LiveImage) => {
215
230
  log_debug(
216
- `playLiveImage: live-image ${playerInfo(
217
- item.player
218
- )}, primary ${playerInfo(this.primaryPlayer)}`
231
+ `playLiveImage: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
219
232
  );
220
233
 
221
234
  if (this.primaryPlayer) {
@@ -223,7 +236,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
223
236
  }
224
237
 
225
238
  if (this.currentlyPlaying) {
226
- if (this.currentlyPlaying?.player?.playerId === item.player.playerId) {
239
+ if (this.currentlyPlaying.playerId === item.playerId) {
227
240
  return;
228
241
  } else {
229
242
  this.pauseItem(this.currentlyPlaying);
@@ -231,18 +244,42 @@ export class LiveImageManager implements PlayerLifecycleListener {
231
244
  }
232
245
 
233
246
  this.currentlyPlaying = item;
234
- item.setMode?.(LiveImageType.Video);
235
247
 
236
- if (item.player.playerState.isReadyToPlay) {
237
- item.player.play();
238
- }
248
+ item
249
+ .prepareForPlayback()
250
+ .then((result) => {
251
+ if (!result) {
252
+ log_error(
253
+ `Failed to prepare live image ${item.playerId} - ${item.tag} for playback: prepareForPlayback returned false`
254
+ );
255
+
256
+ this.currentlyPlaying = null;
257
+ item.setMode?.(LiveImageType.Image);
258
+
259
+ return;
260
+ }
261
+
262
+ // Guard: item might have been replaced while hooks were running
263
+ if (this.currentlyPlaying !== item) return;
264
+
265
+ item.setMode?.(LiveImageType.Video);
266
+
267
+ if (item.player?.playerState.isReadyToPlay) {
268
+ item.player.play();
269
+ }
270
+ })
271
+ .catch((error) => {
272
+ log_error(
273
+ `Failed to prepare live image ${item.playerId} - ${item.tag} for playback: ${error?.message}`
274
+ );
275
+
276
+ this.onLiveImageError(item, error);
277
+ });
239
278
  };
240
279
 
241
280
  public pauseLiveImage = (item: LiveImage) => {
242
281
  log_debug(
243
- `pauseLiveImage: live-image playerId: ${playerInfo(
244
- item.player
245
- )}, primary ${playerInfo(this.primaryPlayer)}`
282
+ `pauseLiveImage: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
246
283
  );
247
284
 
248
285
  this.pauseItem(item);
@@ -258,7 +295,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
258
295
 
259
296
  setUserCellPlayerMutedPreference(true);
260
297
 
261
- this.items.forEach((liveImage) => liveImage.player.mute());
298
+ this.items.forEach((liveImage) => liveImage.mute());
262
299
  };
263
300
 
264
301
  public unmuteAll = () => {
@@ -266,16 +303,36 @@ export class LiveImageManager implements PlayerLifecycleListener {
266
303
 
267
304
  setUserCellPlayerMutedPreference(false);
268
305
 
269
- this.items.forEach((liveImage) => liveImage.player.unmute());
306
+ this.items.forEach((liveImage) => liveImage.unmute());
307
+ };
308
+
309
+ public onViewPositionChanged = (item: LiveImage) => {
310
+ log_debug(
311
+ `onViewPositionChanged: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
312
+ );
313
+
314
+ if (!isTV()) {
315
+ // mobile only
316
+ // we have to delay running checkPlayerPosition, because sometimes on fast scrolling we get wrong order onEnter, then onLeave.
317
+ // which could cause select wrong item to play
318
+
319
+ this.cancelCheckPlayerPositionTimeout();
320
+
321
+ this.checkPlayerPositionTimeout = setTimeout(() => {
322
+ this.cancelCheckPlayerPositionTimeout();
323
+
324
+ this.checkPlayerPosition(item);
325
+ }, TIMEOUT_FOR_DELAY_CHECK_PLAYER_POSITION);
326
+ } else {
327
+ this.checkPlayerPosition(item);
328
+ }
270
329
  };
271
330
 
272
- public checkPlayerPosition = (item: LiveImage) => {
331
+ private checkPlayerPosition = (item: LiveImage) => {
273
332
  this.cancelCheckPlayerPositionTimeout();
274
333
 
275
334
  log_debug(
276
- `checkPlayerPosition: live-image playerId: ${playerInfo(
277
- item.player
278
- )}, primary ${playerInfo(this.primaryPlayer)}`
335
+ `checkPlayerPosition: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
279
336
  );
280
337
 
281
338
  const playerItem = this.findNextPlayableItem();
@@ -391,7 +448,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
391
448
  item,
392
449
  playerId: this.currentlyPlaying?.playerId,
393
450
  primaryPlayerId: this.primaryPlayer?.playerId,
394
- entry: item.getPlayer().getEntry(),
451
+ entry: item.getPlayer()?.getEntry(),
395
452
  },
396
453
  });
397
454
  };
@@ -426,7 +483,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
426
483
  item,
427
484
  playerId: this.currentlyPlaying?.playerId,
428
485
  primaryPlayerId: this.primaryPlayer?.playerId,
429
- entry: item.getPlayer().getEntry(),
486
+ entry: item.getPlayer()?.getEntry(),
430
487
  },
431
488
  });
432
489
  };
@@ -448,7 +505,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
448
505
  this.currentlyPlaying = null;
449
506
 
450
507
  log_debug(
451
- `onLiveImageError: currentitem: ${currentItem.playerId} was removed`
508
+ `onLiveImageError: currentItem: ${currentItem.playerId} - ${currentItem.tag} was removed`
452
509
  );
453
510
 
454
511
  // TODO: ...Maybe player some other item
@@ -460,8 +517,8 @@ export class LiveImageManager implements PlayerLifecycleListener {
460
517
  item,
461
518
  error,
462
519
  playerId: currentItem?.playerId,
463
- primaryPlayerId: currentItem?.playerId,
464
- entry: item.getPlayer().getEntry(),
520
+ primaryPlayerId: this.primaryPlayer?.playerId,
521
+ entry: item.getPlayer()?.getEntry(),
465
522
  },
466
523
  });
467
524
  };
@@ -503,10 +560,10 @@ export class LiveImageManager implements PlayerLifecycleListener {
503
560
  LiveImageManager.instance;
504
561
 
505
562
  export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
506
- public player: Player;
507
- public setMode: (type: LiveImageType) => void;
508
- // Will be replaced with rects
563
+ public player: Player | null = null;
564
+ public setMode?: (type: LiveImageType) => void;
509
565
  public isFullyVisible: boolean = false;
566
+ public tag: string;
510
567
  public position: Position = {
511
568
  centerX: 0,
512
569
  centerY: 0,
@@ -516,18 +573,117 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
516
573
  left: 0,
517
574
  };
518
575
 
576
+ // Keeps muted state in sync: updates the initial value for deferred
577
+ // player creation and forwards to the player if it already exists.
578
+ public initiallyMuted: boolean = true;
579
+
580
+ mute = () => {
581
+ this.initiallyMuted = true;
582
+ this.player?.mute();
583
+ };
584
+
585
+ unmute = () => {
586
+ this.initiallyMuted = false;
587
+ this.player?.unmute();
588
+ };
589
+
590
+ positionToString() {
591
+ if (!this.position) {
592
+ return "position not set";
593
+ }
594
+
595
+ const { centerX, centerY, top, bottom, left, right } = this.position;
596
+
597
+ 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)}`;
598
+ }
599
+
519
600
  readonly playerId: string;
520
- readonly component: Component;
601
+ public component: any = null;
602
+
603
+ private factoryConfig: PlayerFactoryConfig;
604
+ public preloadHooks?: PreloadHookConfig[];
605
+ public processedEntry: ZappEntry | null = null;
606
+ private _preparePromise: Promise<boolean> | null = null;
521
607
 
522
608
  constructor(props: LiveImageProps) {
523
- this.player = props.player;
524
609
  this.setMode = props.setMode;
525
- this.playerId = this.player.playerId;
526
- this.component = props.component;
610
+ this.playerId = props.playerId;
611
+ this.preloadHooks = props.preloadHooks;
612
+ this.factoryConfig = props.factoryConfig;
613
+ this.tag = props.tag || "untagged";
614
+ }
615
+
616
+ async prepareForPlayback(): Promise<boolean> {
617
+ // Already prepared — player exists
618
+ if (this.player) {
619
+ return true;
620
+ }
621
+
622
+ // Deduplicate: if preparation is already in flight, await the same promise
623
+ if (this._preparePromise) {
624
+ return this._preparePromise;
625
+ }
626
+
627
+ this._preparePromise = this.createPreparedPlayer()
628
+ .then((result) => {
629
+ this._preparePromise = null;
630
+
631
+ return result;
632
+ })
633
+ .catch((error) => {
634
+ this._preparePromise = null;
635
+
636
+ log_error(
637
+ `prepareForPlayback: live-image ${this.playerId}, error preparing for playback: ${error?.message}`,
638
+ { error }
639
+ );
640
+
641
+ throw error;
642
+ });
643
+
644
+ return this._preparePromise;
645
+ }
646
+
647
+ private async createPreparedPlayer(): Promise<boolean> {
648
+ let entry = this.factoryConfig.entry;
649
+
650
+ if (this.preloadHooks?.length) {
651
+ const result = await executePreloadHooks({
652
+ preloadHooks: this.preloadHooks,
653
+ entry,
654
+ });
655
+
656
+ if (!result) return false;
657
+
658
+ this.processedEntry = result;
659
+ entry = result;
660
+ }
661
+
662
+ const factoryItem = await playerFactory({
663
+ player: this.factoryConfig.player,
664
+ playerId: this.factoryConfig.playerId,
665
+ autoplay: false,
666
+ entry,
667
+ muted: this.initiallyMuted,
668
+ playerPluginId: this.factoryConfig.playerPluginId,
669
+ screenConfig: this.factoryConfig.screenConfig,
670
+ playerRole: PlayerRole.Cell,
671
+ });
672
+
673
+ if (!factoryItem) {
674
+ throw new Error(
675
+ `Player factory returned null (playerId: ${this.factoryConfig.playerId}, playerPluginId: ${this.factoryConfig.playerPluginId})`
676
+ );
677
+ }
678
+
679
+ this.player = factoryItem.controller;
680
+ this.component = factoryItem.Component;
527
681
  this.player.addListener({ id: "live-image", listener: this });
682
+
683
+ return true;
528
684
  }
529
685
 
530
- public getPlayer = (): Player => {
686
+ public getPlayer = (): Player | null => {
531
687
  return this.player;
532
688
  };
533
689
 
@@ -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,38 @@ 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,
125
+ preloadHooks,
126
+ factoryConfig: {
127
+ player: ref,
128
+ playerId,
129
+ playerPluginId: playerPluginId,
130
+ screenConfig: screenConfig,
131
+ entry: item,
132
+ },
133
+ tag: item.title?.toString(),
133
134
  });
135
+ }, [playerId, preloadHooks, playerPluginId, screenConfig, item]);
134
136
 
135
- if (playerFactoryItem) {
136
- return new LiveImage({
137
- 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]);
137
+ // Keep the muted state in sync with user preference and player state
138
+ liveImageItem.initiallyMuted = muted;
145
139
 
146
140
  React.useEffect(() => {
147
141
  liveImageItem.setMode = setModeDebounced;
148
142
  }, [setModeDebounced, liveImageItem]);
149
143
 
144
+ // todo: no need for it to be react, can be moved into `player.addListener`
150
145
  const { start, end } = React.useMemo(
151
146
  () => getAutoplaySettings(item),
152
147
  [item.id]
153
148
  );
154
149
 
155
- const controller = liveImageItem.getPlayer();
156
150
  const player = usePlayer(playerId);
157
151
 
158
152
  const _assignRoot = (component) => {
@@ -169,7 +163,7 @@ const PlayerLiveImageComponent = (props: Props) => {
169
163
 
170
164
  React.useEffect(() => {
171
165
  // FIXME - find a more elegant way to disable live-image on cell for measurement
172
- if (isMeasurement(item)) {
166
+ if (isMeasurement(item.id)) {
173
167
  return;
174
168
  }
175
169
 
@@ -253,7 +247,13 @@ const PlayerLiveImageComponent = (props: Props) => {
253
247
  }, [item.id]);
254
248
 
255
249
  React.useEffect(() => {
256
- if (isMeasurement(item) || !playerManager) {
250
+ if (isMeasurement(item.id) || !playerManager) {
251
+ return;
252
+ }
253
+
254
+ const controller = liveImageItem.getPlayer();
255
+
256
+ if (!controller) {
257
257
  return;
258
258
  }
259
259
 
@@ -273,7 +273,7 @@ const PlayerLiveImageComponent = (props: Props) => {
273
273
  playerManager.unregisterPlayer(playerId);
274
274
  };
275
275
  }
276
- }, [liveImageItem, playerId, controller, item.id]);
276
+ }, [liveImageItem, playerId, mode, item.id]);
277
277
 
278
278
  const onPositionUpdated = React.useCallback(
279
279
  (data) => {
@@ -310,7 +310,7 @@ const PlayerLiveImageComponent = (props: Props) => {
310
310
  ? LiveImageManager.instance.onViewportEnter(liveImageItem)
311
311
  : LiveImageManager.instance.onViewportLeave(liveImageItem);
312
312
  } else {
313
- LiveImageManager.instance.checkPlayerPosition(liveImageItem);
313
+ LiveImageManager.instance.onViewPositionChanged(liveImageItem);
314
314
  }
315
315
  },
316
316
  [liveImageItem, dimensions.height, dimensions.width]
@@ -338,11 +338,11 @@ const PlayerLiveImageComponent = (props: Props) => {
338
338
  <Player
339
339
  autoplay={false}
340
340
  ref={_assignRoot}
341
- entry={item} // Must be passed first in list
341
+ entry={liveImageItem.processedEntry || item} // Must be passed first in list
342
342
  style={videoStyles}
343
343
  playerId={playerId}
344
344
  muted={muted}
345
- listener={controller?.getListener()}
345
+ listener={liveImageItem.getPlayer()?.getListener()}
346
346
  resizeMode={"cover"}
347
347
  {...platformSpecificProps}
348
348
  />
@@ -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
  );
@@ -4,10 +4,7 @@ import { Animated, Dimensions } from "react-native";
4
4
  import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation";
5
5
 
6
6
  import { useConfiguration } from "../utils";
7
- import {
8
- useAppData,
9
- usePlugins,
10
- } from "@applicaster/zapp-react-native-redux/hooks";
7
+ import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
11
8
  import { isBottomTabVisible } from "../../Screen/navigationHandler";
12
9
 
13
10
  import {
@@ -21,6 +18,7 @@ import {
21
18
  import { getTabBarHeight } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/getTabBarHeight";
22
19
  import { PROGRESS_BAR_HEIGHT } from "./utils";
23
20
  import { useIsTablet as getIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
21
+ import { useAppSelector } from "@applicaster/zapp-react-native-redux";
24
22
 
25
23
  export type ModalAnimationContextT = {
26
24
  yTranslate: React.MutableRefObject<Animated.AnimatedInterpolation<number>>;
@@ -66,7 +64,9 @@ const Provider = ({ children }: { children: React.ReactNode }) => {
66
64
  screenData,
67
65
  } = useNavigation();
68
66
 
69
- const { isTabletPortrait } = useAppData();
67
+ const isTabletPortrait = useAppSelector(
68
+ (state) => state.appData.isTabletPortrait
69
+ );
70
70
 
71
71
  const plugins = usePlugins();
72
72
 
@@ -1,4 +1,4 @@
1
- import { act, renderHook, waitFor } from "@testing-library/react-native";
1
+ import { renderHook } from "@testing-library/react-hooks";
2
2
  import { useDelayedPlayerDetails } from "../useDelayedPlayerDetails";
3
3
  import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
4
4
 
@@ -13,20 +13,16 @@ describe("useDelayedPlayerDetails", () => {
13
13
  jest.clearAllMocks();
14
14
  });
15
15
 
16
- it("should return false initially, that changes after 1 second", async () => {
16
+ it("should return false initially, that changes after 1 second", () => {
17
17
  const { result } = renderHook(() =>
18
18
  useDelayedPlayerDetails({ isInline: true, isDocked: false, isPip: false })
19
19
  );
20
20
 
21
21
  expect(result.current).toBe(false);
22
22
 
23
- act(() => {
24
- jest.advanceTimersByTime(1000);
25
- });
23
+ jest.advanceTimersByTime(1000);
26
24
 
27
- await waitFor(() => {
28
- expect(result.current).toBe(true);
29
- });
25
+ expect(result.current).toBe(true);
30
26
  });
31
27
 
32
28
  it("should return false when isPip is true", () => {
@@ -45,7 +41,7 @@ describe("useDelayedPlayerDetails", () => {
45
41
  expect(result.current).toBe(false);
46
42
  });
47
43
 
48
- it("should return true for tablet regardless of other flags", async () => {
44
+ it("should return true for tablet regardless of other flags", () => {
49
45
  (useIsTablet as jest.Mock).mockReturnValue(true);
50
46
 
51
47
  const { result } = renderHook(() =>
@@ -56,12 +52,8 @@ describe("useDelayedPlayerDetails", () => {
56
52
  })
57
53
  );
58
54
 
59
- act(() => {
60
- jest.advanceTimersByTime(1000);
61
- });
55
+ jest.advanceTimersByTime(1000);
62
56
 
63
- await waitFor(() => {
64
- expect(result.current).toBe(true);
65
- });
57
+ expect(result.current).toBe(true);
66
58
  });
67
59
  });