@applicaster/quick-brick-player 13.0.0-rc.7 → 13.0.0-rc.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-player",
3
- "version": "13.0.0-rc.7",
3
+ "version": "13.0.0-rc.71",
4
4
  "description": "Quick Brick Player",
5
5
  "main": "./src/index.ts",
6
6
  "types": "index.d.ts",
@@ -36,11 +36,11 @@
36
36
  },
37
37
  "homepage": "https://github.com/applicaster/quickbrick#readme",
38
38
  "dependencies": {
39
- "@applicaster/quick-brick-mobile-transport-controls": "13.0.0-rc.6",
40
- "@applicaster/quick-brick-tv-transport-controls": "12.0.0-rc.112",
41
- "@applicaster/zapp-react-native-tvos-app": "13.0.0-rc.7",
42
- "@applicaster/zapp-react-native-ui-components": "13.0.0-rc.7",
43
- "@applicaster/zapp-react-native-utils": "13.0.0-rc.7",
39
+ "@applicaster/quick-brick-mobile-transport-controls": "13.0.0-rc.69",
40
+ "@applicaster/quick-brick-tv-transport-controls": "13.0.0-rc.56",
41
+ "@applicaster/zapp-react-native-tvos-app": "13.0.0-rc.71",
42
+ "@applicaster/zapp-react-native-ui-components": "13.0.0-rc.71",
43
+ "@applicaster/zapp-react-native-utils": "13.0.0-rc.71",
44
44
  "keymirror": "^0.1.1",
45
45
  "query-string": "7.1.3",
46
46
  "react-native-web-linear-gradient": "1.1.2",
@@ -51,12 +51,28 @@
51
51
  "videojs-contrib-quality-levels": "3.0.0"
52
52
  },
53
53
  "peerDependencies": {
54
+ "@applicaster/applicaster-types": "*",
55
+ "@applicaster/blur": "*",
54
56
  "@applicaster/quick-brick-xray": "*",
55
57
  "@applicaster/react-native-color-thief": "*",
56
- "@types/node": "*",
57
- "@types/react-native": "0.69.6",
58
- "react": "17.0.2",
59
- "react-native": "0.68.6",
60
- "react-native-linear-gradient": "*"
58
+ "@applicaster/zapp-pipes-v2-client": "*",
59
+ "@applicaster/zapp-react-dom-ui-components": "*",
60
+ "@applicaster/zapp-react-native-redux": "*",
61
+ "@applicaster/zapp-react-native-tvos-ui-components": "*",
62
+ "@babel/runtime": "*",
63
+ "@react-native-community/netinfo": "*",
64
+ "immer": "*",
65
+ "react": "*",
66
+ "react-native": "*",
67
+ "react-native-fs": "*",
68
+ "react-native-gesture-handler": "*",
69
+ "react-native-linear-gradient": "*",
70
+ "react-native-safe-area-context": "*",
71
+ "react-native-svg": "*",
72
+ "react-native-web": "*",
73
+ "react-native-webview": "*",
74
+ "uglify-js": "*",
75
+ "validate-color": "*",
76
+ "zustand": "*"
61
77
  }
62
78
  }
@@ -55,6 +55,11 @@ const useMuteUnmuteListeners = (playerId, player, asset, setImageUri) =>
55
55
  return;
56
56
  }
57
57
 
58
+ // initial state setup - show correct mute/unmute icon based on current state
59
+ if (player?.playerState?.isReadyToPlay) {
60
+ setImageUri(player.getIsMuted() ? asset.active : asset.inactive);
61
+ }
62
+
58
63
  // active action = muted, inactive = unmuted
59
64
  return player.addListener({
60
65
  id: liveImageButtonsListenerKey,
@@ -73,10 +78,17 @@ const useMuteUnmuteListeners = (playerId, player, asset, setImageUri) =>
73
78
  setImageUri(player?.getIsMuted() ? asset.active : asset.inactive);
74
79
  },
75
80
  onPlayerClose: () => {
76
- setImageUri(null);
81
+ if (isCurrentPlayer(playerId)) {
82
+ setImageUri(null);
83
+ }
77
84
  },
78
85
  onVideoEnd: () => {
79
- setImageUri(null);
86
+ if (isCurrentPlayer(playerId)) {
87
+ setImageUri(null);
88
+ }
89
+ },
90
+ onPlayerResume: () => {
91
+ setImageUri(player?.getIsMuted() ? asset.active : asset.inactive);
80
92
  },
81
93
  },
82
94
  });
@@ -93,7 +105,7 @@ const styles = StyleSheet.create({
93
105
  },
94
106
  });
95
107
 
96
- export function AssetComponent(props: CellActionAssetComponentProps) {
108
+ function AssetImage(props: CellActionAssetComponentProps) {
97
109
  const { cellUUID: playerId, asset } = props;
98
110
  const [imageUri, setImageUri] = useState<Asset>(null);
99
111
 
@@ -108,3 +120,5 @@ export function AssetComponent(props: CellActionAssetComponentProps) {
108
120
 
109
121
  return <View />;
110
122
  }
123
+
124
+ export const AssetComponent = React.memo(AssetImage);
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { ViewStyle, View } from "react-native";
2
+ import { ViewStyle, View, Dimensions as ScreenDimensions } from "react-native";
3
3
 
4
4
  import { SafeAreaView, Edge } from "react-native-safe-area-context";
5
5
  import { usePlayerState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState";
@@ -17,13 +17,7 @@ import { PlayerDetails } from "@applicaster/zapp-react-native-ui-components/Comp
17
17
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
18
18
  import { useEffect, useState } from "react";
19
19
  import { MagicBG } from "./MagicBG";
20
- import {
21
- formatSpeed,
22
- hasDatePassed,
23
- loadItemsFromStorage,
24
- NAMESPACE,
25
- } from "./utils";
26
- import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
20
+ import { formatSpeed } from "./utils";
27
21
  import { addQueryParam, getLiveUrl } from "../Utils";
28
22
  import { useTitleSummaryOverlay } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerTitleSummaryOverlay";
29
23
  import {
@@ -33,6 +27,21 @@ import {
33
27
  } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
34
28
  import { PADDING_HORIZONTAL } from "./Layout/defaultLayoutsConts";
35
29
  import { toNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
30
+ import {
31
+ speed_0_8,
32
+ speed_1,
33
+ speed_1_2,
34
+ speed_1_5,
35
+ speed_2,
36
+ } from "@applicaster/quick-brick-mobile-transport-controls";
37
+ import {
38
+ SleepTimerController,
39
+ SleepTimerState,
40
+ SLEEP_TIMER_OFF,
41
+ } from "./SleepTimerManager";
42
+ import { SpeedController, PlaybackSpeed, DEFAULT_SPEED } from "./SpeedManager";
43
+ import { imageSrcFromMediaItem } from "@applicaster/zapp-react-native-utils/configurationUtils";
44
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
36
45
 
37
46
  const { log_debug } = createLogger({
38
47
  subsystem: "UIComponent",
@@ -50,6 +59,7 @@ type Configuration = {
50
59
  [key: string]: any;
51
60
  magic_background?: boolean;
52
61
  audio_player_background_image_query?: string;
62
+ audio_player_background_image_default_color?: string;
53
63
  tablet_landscape_sidebar_width?: string;
54
64
  tablet_landscape_player_container_background_color?: string;
55
65
  };
@@ -71,8 +81,7 @@ type Props = {
71
81
  isTabletPortrait?: boolean;
72
82
  };
73
83
 
74
- export const defaultAspectRatioWidth = (height: number): number =>
75
- (height / 9) * 16;
84
+ const { width: SCREEN_WIDTH } = ScreenDimensions.get("screen");
76
85
 
77
86
  const Styles = {
78
87
  mainContainer: (style: ViewStyle) => {
@@ -98,7 +107,11 @@ const Styles = {
98
107
  height: 96,
99
108
  marginHorizontal: 24,
100
109
  },
101
- playerDetails: { marginTop: 60 },
110
+ playerDetails: {
111
+ marginTop: 60,
112
+ alignSelf: "center",
113
+ width: SCREEN_WIDTH - PADDING_HORIZONTAL * 2,
114
+ },
102
115
 
103
116
  linearGradient: {
104
117
  flex: 1,
@@ -116,9 +129,12 @@ const sleeps = [
116
129
  { label: "2 hours", value: 120 },
117
130
  ];
118
131
 
132
+ const edges: Edge[] = ["right", "left", "top"];
133
+
119
134
  export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
120
135
  const player = usePlayer();
121
136
  const playerState = usePlayerState("audio-player-wrapper");
137
+ const theme = useTheme();
122
138
 
123
139
  const {
124
140
  docked,
@@ -132,11 +148,11 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
132
148
  } = props;
133
149
 
134
150
  const speeds = [
135
- { asset: configuration["speed_0_8"], value: 0.8 },
136
- { asset: configuration["speed_1"], value: 1.0 },
137
- { asset: configuration["speed_1_2"], value: 1.2 },
138
- { asset: configuration["speed_1_5"], value: 1.5 },
139
- { asset: configuration["speed_2"], value: 2.0 },
151
+ { asset: configuration["speed_0_8"] || speed_0_8, value: 0.8 },
152
+ { asset: configuration["speed_1"] || speed_1, value: 1.0 },
153
+ { asset: configuration["speed_1_2"] || speed_1_2, value: 1.2 },
154
+ { asset: configuration["speed_1_5"] || speed_1_5, value: 1.5 },
155
+ { asset: configuration["speed_2"] || speed_2, value: 2.0 },
140
156
  ];
141
157
 
142
158
  const isTablet = useIsTablet();
@@ -154,17 +170,29 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
154
170
  configuration?.audio_live_player_update_interval
155
171
  );
156
172
 
157
- const [playbackSpeed, setPlaybackSpeed] = useState({
158
- asset: configuration["speed_1"],
159
- value: 1.0,
160
- });
173
+ const [sleepTimerManager, setSleepTimerManager] =
174
+ useState<SleepTimerController | null>(null);
161
175
 
162
- const [sleepTimer, setSleepTimer] = useState({
163
- label: "playback_sleep_off_label",
164
- value: 0,
165
- });
176
+ const [speedManager, setSpeedManager] = useState<SpeedController | null>(
177
+ null
178
+ );
179
+
180
+ useEffect(() => {
181
+ if (player) {
182
+ setSleepTimerManager(new SleepTimerController(player));
183
+ setSpeedManager(new SpeedController(player));
184
+ } else {
185
+ setSleepTimerManager(null);
186
+ setSpeedManager(null);
187
+ }
188
+ }, [player]);
189
+
190
+ const [playbackSpeed, setPlaybackSpeed] =
191
+ useState<PlaybackSpeed>(DEFAULT_SPEED);
166
192
 
167
- const [sleepTimerDate, setSleepTimerDate] = useState(0);
193
+ const [sleepTimerState, setSleepTimerState] = useState<SleepTimerState>(
194
+ new SleepTimerState(SLEEP_TIMER_OFF, 0)
195
+ );
168
196
 
169
197
  const [defaultDimensions, setDefaultDimensions] =
170
198
  React.useState<Dimensions>(undefined);
@@ -189,82 +217,27 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
189
217
  if (!defaultDimensions) setDefaultDimensions({ width, height });
190
218
  };
191
219
 
192
- const cancelSleepTimer = async () => {
193
- player?.cancelSleepTimer();
194
-
195
- setSleepTimer({
196
- label: "playback_sleep_off_label",
197
- value: 0,
198
- });
199
-
200
- setSleepTimerDate(0);
201
-
202
- await localStorage.removeItem("sleep", NAMESPACE);
203
- };
204
-
205
- const startSleepTimer = (timerValue: number) => {
206
- try {
207
- const currentDate = new Date();
208
- const futureDate = new Date(currentDate.getTime() + timerValue * 60000);
209
- const futureSleepDate = futureDate.getTime();
210
-
211
- player?.cancelSleepTimer();
212
-
213
- player?.startSleepTimer(futureSleepDate);
214
-
215
- return futureSleepDate;
216
- } catch (e) {
217
- return 0;
218
- }
219
- };
220
-
221
- const handleSleep = async (
222
- timerValue: number,
223
- currentFutureSleepDate = null
224
- ) => {
225
- if (currentFutureSleepDate) {
226
- if (hasDatePassed(currentFutureSleepDate)) {
227
- await cancelSleepTimer();
228
-
229
- return 0;
230
- }
231
-
232
- return currentFutureSleepDate;
233
- }
234
-
235
- if (timerValue === 0) {
236
- await cancelSleepTimer();
237
-
238
- return timerValue;
239
- }
240
-
241
- return startSleepTimer(timerValue);
242
- };
243
-
244
220
  useEffect(() => {
245
- const loadSpeed = async () => {
246
- const speed = await loadItemsFromStorage("speed");
247
-
248
- if (playerState.isReadyToPlay && speed) {
249
- setPlaybackSpeed(speed);
250
- player?.setPlaybackRate(speed.value);
251
- }
252
- };
221
+ const sleepSubscription =
222
+ sleepTimerManager?.sleepTimerState$.subscribe(setSleepTimerState);
253
223
 
254
- const loadSleep = async () => {
255
- const sleep = await loadItemsFromStorage("sleep");
224
+ const speedSubscription =
225
+ speedManager?.playbackSpeed$.subscribe(setPlaybackSpeed);
256
226
 
257
- if (playerState.isReadyToPlay && sleep) {
258
- const { futureSleepDate, ...rest } = sleep;
259
- setSleepTimer(rest);
260
- const sleepTimerDate = await handleSleep(sleep.value, futureSleepDate);
261
- setSleepTimerDate(sleepTimerDate);
262
- }
227
+ return () => {
228
+ sleepSubscription?.unsubscribe();
229
+ speedSubscription?.unsubscribe();
263
230
  };
231
+ }, [sleepTimerManager, speedManager]);
232
+
233
+ useEffect(() => {
234
+ if (!playerState.isReadyToPlay) {
235
+ return;
236
+ }
264
237
 
265
- loadSpeed().then();
266
- loadSleep().then();
267
- }, [playerState.isReadyToPlay]);
238
+ void speedManager?.loadSpeed();
239
+ void sleepTimerManager?.loadSleep();
240
+ }, [playerState.isReadyToPlay, speedManager, sleepTimerManager]);
268
241
 
269
242
  const getValue = (key: string, stylesObj: {}) =>
270
243
  R.propOr(null, key, stylesObj);
@@ -292,7 +265,7 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
292
265
 
293
266
  const onPlaybackButtonPress = React.useCallback(() => {
294
267
  if (playerState.isBuffering) return;
295
- playerState.isPaused ? player?.play() : player.pause();
268
+ playerState.isPaused ? player?.play() : player?.pause();
296
269
  }, [playerState, player]);
297
270
 
298
271
  const seekDuration = toNumberWithDefault(10, value("seek_duration"));
@@ -307,26 +280,16 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
307
280
 
308
281
  const onSpeedSelect = React.useCallback(
309
282
  async (item) => {
310
- setPlaybackSpeed(item);
311
- player.setPlaybackRate(item.value);
312
- await localStorage.setItem("speed", item, NAMESPACE);
283
+ await speedManager?.setPlaybackSpeed(item);
313
284
  },
314
- [player, setPlaybackSpeed]
285
+ [speedManager]
315
286
  );
316
287
 
317
288
  const onSleepSelect = React.useCallback(
318
289
  async (item) => {
319
- setSleepTimer(item);
320
- const futureSleepDate = await handleSleep(item.value);
321
- setSleepTimerDate(futureSleepDate);
322
-
323
- await localStorage.setItem(
324
- "sleep",
325
- { ...item, futureSleepDate },
326
- NAMESPACE
327
- );
290
+ await sleepTimerManager?.setSleepTimer(item);
328
291
  },
329
- [player, setSleepTimer]
292
+ [sleepTimerManager]
330
293
  );
331
294
 
332
295
  const onSpeedButtonPress = React.useCallback(() => {
@@ -341,15 +304,24 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
341
304
  supportedOrientations: ["portrait"],
342
305
  },
343
306
  });
344
- }, [playbackSpeed, setPlaybackSpeed]);
307
+ }, [
308
+ playbackSpeed,
309
+ setPlaybackSpeed,
310
+ navigator,
311
+ onSpeedSelect,
312
+ configuration["playback_speed_title"],
313
+ ]);
345
314
 
346
315
  const onSleepButtonPress = React.useCallback(() => {
347
316
  navigator.openModalBottomSheet({
348
317
  modalBottomSheetContentProps: {
349
318
  items: [
350
- { label: configuration["playback_sleep_off_label"], value: 0 },
319
+ {
320
+ label: configuration["playback_sleep_off_label"],
321
+ value: 0,
322
+ },
351
323
  ].concat(sleeps),
352
- current_selection: sleepTimer,
324
+ current_selection: sleepTimerState.timer,
353
325
  onPress: onSleepSelect,
354
326
  title: configuration["playback_sleep_title"],
355
327
  },
@@ -357,7 +329,21 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
357
329
  supportedOrientations: ["portrait"],
358
330
  },
359
331
  });
360
- }, [sleepTimer, setSleepTimer, configuration]);
332
+ }, [
333
+ navigator,
334
+ sleepTimerState.timer,
335
+ configuration["playback_sleep_title"],
336
+ configuration["playback_sleep_off_label"],
337
+ onSleepSelect,
338
+ ]);
339
+
340
+ // This method does not actually cancel the sleep timer
341
+ // (and should not to avoid race condition),
342
+ // it just performs the cleanup when timer expires
343
+ // TODO: rename it in code above
344
+ const cancelSleepTimer = React.useCallback(() => {
345
+ sleepTimerManager?.onTimerExpired();
346
+ }, [sleepTimerManager]);
361
347
 
362
348
  const onCloseVideoModal = React.useCallback(() => {
363
349
  playerManager.close();
@@ -378,8 +364,6 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
378
364
  player?.seekTo(0);
379
365
  }, [player]);
380
366
 
381
- const edges = React.useMemo<Edge[]>(() => ["right", "left", "top"], [docked]);
382
-
383
367
  const ContainerView = SafeAreaView;
384
368
 
385
369
  const MainContent = React.useMemo(() => {
@@ -406,9 +390,11 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
406
390
  onSleepButtonPress={onSleepButtonPress}
407
391
  playbackSpeedName={formatSpeed(playbackSpeed.value)}
408
392
  playbackSleepName={
409
- sleepTimer.value === 0 ? "sleep_timer" : "sleep_timer_active"
393
+ sleepTimerState.timer.value === 0
394
+ ? "sleep_timer"
395
+ : "sleep_timer_active"
410
396
  }
411
- sleepTimerDate={sleepTimerDate}
397
+ sleepTimerDate={sleepTimerState.timerDate}
412
398
  cancelSleepTimer={cancelSleepTimer}
413
399
  onSkipLive={onSkipLive}
414
400
  onCloseVideoModal={onCloseVideoModal}
@@ -458,26 +444,31 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
458
444
  playNextData,
459
445
  onSkipStart,
460
446
  onSkipLive,
461
- sleepTimer.value,
462
- sleepTimerDate,
447
+ sleepTimerState.timer.value,
448
+ sleepTimerState.timerDate,
463
449
  ]);
464
450
 
465
451
  if (configuration.magic_background) {
466
- const baseUrl = entry?.extensions?.audio_player_background_image ?? "";
452
+ const baseUrl = imageSrcFromMediaItem(entry, null);
467
453
 
468
454
  const queryParamName =
469
455
  configuration.audio_player_background_image_query || "width";
470
456
 
457
+ const defaultBackgroundColor =
458
+ configuration.audio_player_background_image_default_color ||
459
+ theme?.app_background_color;
460
+
471
461
  const queryParamValue = 15;
472
462
 
473
- const url = addQueryParam(baseUrl, queryParamName, queryParamValue);
463
+ const url =
464
+ baseUrl && addQueryParam(baseUrl, queryParamName, queryParamValue);
474
465
 
475
466
  return (
476
467
  <MagicBG
477
468
  image={url}
478
469
  fallbackUrl={baseUrl}
479
470
  colors={entry?.extensions?.backgroundColors}
480
- defaultColor="#000000"
471
+ defaultColor={defaultBackgroundColor}
481
472
  >
482
473
  {MainContent}
483
474
  </MagicBG>
@@ -1,13 +1,14 @@
1
1
  import * as React from "react";
2
+ import { useEffect, useMemo, useState } from "react";
2
3
  import * as R from "ramda";
3
- import { View } from "react-native";
4
+ import { View, Dimensions as ScreenDimensions } from "react-native";
4
5
  import { dayjs } from "@applicaster/zapp-react-native-utils/dateUtils";
5
6
 
6
7
  import {
7
- ProgressBar,
8
8
  Controls,
9
- TransportInfo,
10
9
  CountDown,
10
+ ProgressBar,
11
+ TransportInfo,
11
12
  } from "@applicaster/quick-brick-mobile-transport-controls";
12
13
 
13
14
  import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
@@ -15,26 +16,30 @@ import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
15
16
  import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
16
17
  import {
17
18
  forwardButton,
19
+ PADDING_HORIZONTAL,
18
20
  playbackButton,
19
21
  rewindButton,
20
- skipToStartButton,
21
22
  skipToLiveButton,
22
- PADDING_HORIZONTAL,
23
- speedButton,
23
+ skipToStartButton,
24
24
  sleepButton,
25
+ speedButton,
25
26
  } from "./defaultLayoutsConts";
26
27
  import { AudioDescription } from "../AudioDescription";
27
28
  import { PlayerImageBackground } from "@applicaster/zapp-react-native-ui-components/Components/PlayerImageBackground";
28
29
  import { useLanguageData } from "@applicaster/zapp-react-native-utils/reactHooks/player";
29
30
  import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
30
- import { useEffect, useMemo, useState } from "react";
31
31
  import {
32
- useModalAnimationContext,
33
32
  AnimationComponent,
34
33
  ComponentAnimationType,
34
+ useModalAnimationContext,
35
35
  } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
36
+ import { PlayerAnimationStateT } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation/ModalAnimationContext";
36
37
  import DockedLayout from "./DockedLayout";
37
38
  import { isValidDate } from "../../Utils";
39
+ import { calculateBorderRadius } from "../utils";
40
+
41
+ const getAudioPlayerWidthForTablet = (isPortraitMode: boolean) =>
42
+ isPortraitMode ? 463 : 350;
38
43
 
39
44
  type Dimensions = {
40
45
  width: number;
@@ -79,6 +84,9 @@ type LayoutProps = {
79
84
  defaultDimensions: Dimensions;
80
85
  };
81
86
 
87
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } =
88
+ ScreenDimensions.get("screen");
89
+
82
90
  const Styles = {
83
91
  container: { flex: 1 },
84
92
  tabletContainer: (isPortraitMode: boolean) => ({
@@ -86,42 +94,63 @@ const Styles = {
86
94
  alignItems: "center",
87
95
  justifyContent: isPortraitMode ? "center" : "flex-start",
88
96
  }),
89
- playerImageWrapper: {
97
+ playerImageWrapper: (docked: boolean) => ({
90
98
  width: "100%",
91
99
  maxHeight: 750,
92
- marginTop: 80,
100
+ marginTop: docked ? 80 : 20,
101
+ }),
102
+ playerInfoWrapper: {
103
+ flex: 1,
104
+ alignItems: "center",
105
+ justifyContent: "space-between",
93
106
  },
94
- tabletLayout: {
95
- width: 463,
107
+ tabletLayout: (isPortraitMode: boolean) => ({
108
+ width: getAudioPlayerWidthForTablet(isPortraitMode),
96
109
  marginBottom: 80,
97
- },
110
+ paddingHorizontal: undefined, // override paddingHorizontal
111
+ }),
98
112
  controls: (isTablet: boolean, isLiveShowFullControls: boolean) => ({
99
113
  height: isTablet ? 64 : 50,
100
- marginHorizontal: 10,
114
+ marginHorizontal: isTablet ? undefined : 10,
101
115
  justifyContent: isLiveShowFullControls ? "space-between" : "space-evenly",
102
116
  }),
103
- backgroundImageContainer: (dimensions: Dimensions, isTablet: boolean) => {
104
- const mobileValue = dimensions.width
117
+ backgroundImageContainer: (
118
+ dimensions: Dimensions,
119
+ isTablet: boolean,
120
+ isPortraitMode: boolean
121
+ ) => {
122
+ if (isTablet) {
123
+ const size = getAudioPlayerWidthForTablet(isPortraitMode);
124
+
125
+ return {
126
+ width: size,
127
+ height: size,
128
+ };
129
+ }
130
+
131
+ const mobileValue = dimensions?.width
105
132
  ? dimensions.width - PADDING_HORIZONTAL * 2
106
- : 330;
133
+ : SCREEN_WIDTH - PADDING_HORIZONTAL * 2;
107
134
 
108
135
  return {
109
- width: isTablet ? 463 : mobileValue,
110
- height: isTablet ? 463 : mobileValue,
136
+ width: mobileValue,
137
+ height: mobileValue,
111
138
  };
112
139
  },
113
140
  backgroundImageContainerImageStyle: (
114
141
  isTablet: boolean,
115
142
  docked: boolean,
116
143
  startComponentsAnimation: boolean,
117
- isActiveGesture: boolean
144
+ isActiveGesture: boolean,
145
+ playerAnimationState: PlayerAnimationStateT
118
146
  ) => ({
119
- borderRadius:
120
- (startComponentsAnimation && isActiveGesture) || docked
121
- ? 0
122
- : isTablet
123
- ? 24
124
- : 10,
147
+ borderRadius: calculateBorderRadius(
148
+ isTablet,
149
+ docked,
150
+ startComponentsAnimation,
151
+ isActiveGesture,
152
+ playerAnimationState
153
+ ),
125
154
  }),
126
155
 
127
156
  paddingHorizontal: {
@@ -173,8 +202,7 @@ export default function DefaultLayout(props: LayoutProps) {
173
202
  } = props;
174
203
 
175
204
  const isTablet = useIsTablet();
176
- const { width, height } = dimensions;
177
- const isPortraitMode = width < height;
205
+ const isPortraitMode = SCREEN_WIDTH < SCREEN_HEIGHT;
178
206
  const isLiveContent = playerState.isLive;
179
207
  const docked = layoutState.docked;
180
208
 
@@ -330,12 +358,15 @@ export default function DefaultLayout(props: LayoutProps) {
330
358
 
331
359
  const defaultImageDimensions = React.useMemo(
332
360
  () =>
333
- defaultDimensions &&
334
- Styles.backgroundImageContainer(defaultDimensions, isTablet),
335
- [defaultDimensions, isTablet]
361
+ Styles.backgroundImageContainer(
362
+ defaultDimensions,
363
+ isTablet,
364
+ isPortraitMode
365
+ ),
366
+ [defaultDimensions, isTablet, isPortraitMode]
336
367
  );
337
368
 
338
- const { isActiveGesture, startComponentsAnimation } =
369
+ const { isActiveGesture, startComponentsAnimation, playerAnimationState } =
339
370
  useModalAnimationContext();
340
371
 
341
372
  return (
@@ -344,7 +375,10 @@ export default function DefaultLayout(props: LayoutProps) {
344
375
  isTablet ? Styles.tabletContainer(isPortraitMode) : Styles.container
345
376
  }
346
377
  >
347
- <AnimationComponent animationType={ComponentAnimationType.componentFade}>
378
+ <AnimationComponent
379
+ animationType={ComponentAnimationType.componentFade}
380
+ style={isTablet ? Styles.playerInfoWrapper : undefined}
381
+ >
348
382
  <TransportInfo
349
383
  content={entry}
350
384
  value={value}
@@ -356,13 +390,16 @@ export default function DefaultLayout(props: LayoutProps) {
356
390
  </AnimationComponent>
357
391
  <AnimationComponent
358
392
  animationType={ComponentAnimationType.moveUpComponent}
359
- style={isTablet ? Styles.playerImageWrapper : undefined}
393
+ style={isTablet ? Styles.playerImageWrapper(docked) : undefined}
360
394
  >
361
395
  <AnimationComponent
362
396
  animationType={ComponentAnimationType.moveComponentHorizontal}
363
397
  additionalData={{
364
398
  disableAnimatedComponent: !isTablet,
365
- moveValue: (dimensions.width - 463) / 2,
399
+ moveValue:
400
+ (dimensions.width -
401
+ getAudioPlayerWidthForTablet(isPortraitMode)) /
402
+ 2,
366
403
  extraAnimation: !isTablet
367
404
  ? {
368
405
  [ComponentAnimationType.audioPlayerPaddingHorizontal]: {
@@ -375,12 +412,17 @@ export default function DefaultLayout(props: LayoutProps) {
375
412
  >
376
413
  <PlayerImageBackground
377
414
  entry={entry}
378
- style={Styles.backgroundImageContainer(dimensions, isTablet)}
415
+ style={Styles.backgroundImageContainer(
416
+ dimensions,
417
+ isTablet,
418
+ isPortraitMode
419
+ )}
379
420
  imageStyle={Styles.backgroundImageContainerImageStyle(
380
421
  isTablet,
381
422
  docked,
382
423
  startComponentsAnimation,
383
- isActiveGesture
424
+ isActiveGesture,
425
+ playerAnimationState
384
426
  )}
385
427
  defaultImageDimensions={defaultImageDimensions}
386
428
  />
@@ -391,7 +433,7 @@ export default function DefaultLayout(props: LayoutProps) {
391
433
  <View
392
434
  style={[
393
435
  Styles.paddingHorizontal,
394
- isTablet ? Styles.tabletLayout : undefined,
436
+ isTablet ? Styles.tabletLayout(isPortraitMode) : undefined,
395
437
  ]}
396
438
  >
397
439
  {!docked && (
@@ -405,9 +447,10 @@ export default function DefaultLayout(props: LayoutProps) {
405
447
  layoutState={layoutState}
406
448
  />
407
449
  <Separator />
408
- {needsToShowProgressBar && (
450
+ {needsToShowProgressBar ? (
409
451
  <View>
410
452
  <ProgressBar
453
+ name={"audio-default-layout"}
411
454
  content={entry}
412
455
  value={value}
413
456
  visible={true}
@@ -422,17 +465,17 @@ export default function DefaultLayout(props: LayoutProps) {
422
465
  onSkipLive={onSkipLive}
423
466
  />
424
467
 
425
- {showCounter && timeLeft >= 0 && (
468
+ {showCounter && timeLeft >= 0 ? (
426
469
  <CountDown
427
470
  timeLeft={timeLeft}
428
471
  value={value}
429
472
  title={sleepTitle}
430
473
  />
431
- )}
474
+ ) : null}
432
475
 
433
476
  <Separator />
434
477
  </View>
435
- )}
478
+ ) : null}
436
479
  <Controls
437
480
  value={value}
438
481
  visible={true}
@@ -9,18 +9,18 @@ import * as R from "ramda";
9
9
  import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
10
10
 
11
11
  import {
12
+ Button,
12
13
  ContentInfo,
13
- ProgressBar,
14
14
  Controls,
15
- Button,
15
+ ProgressBar,
16
16
  } from "@applicaster/quick-brick-mobile-transport-controls";
17
17
 
18
18
  import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
19
19
  import {
20
- useModalAnimationContext,
21
20
  AnimationComponent,
22
21
  ComponentAnimationType,
23
22
  PlayerAnimationStateEnum,
23
+ useModalAnimationContext,
24
24
  } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
25
25
 
26
26
  type Dimensions = {
@@ -230,7 +230,7 @@ export default function DockedLayout(props: LayoutProps) {
230
230
  } = useModalAnimationContext();
231
231
 
232
232
  const maximazeVideoModal = React.useCallback(
233
- () => setPlayerAnimationState(PlayerAnimationStateEnum.maximaze),
233
+ () => setPlayerAnimationState(PlayerAnimationStateEnum.maximize),
234
234
  []
235
235
  );
236
236
 
@@ -245,18 +245,19 @@ export default function DockedLayout(props: LayoutProps) {
245
245
  ]}
246
246
  >
247
247
  {!playerState.isLive &&
248
- (layoutState.docked || startComponentsAnimation) && (
249
- <ProgressBar
250
- content={entry}
251
- value={value}
252
- visible={visible}
253
- layoutState={layoutState}
254
- onPlayerSeek={onPlayerSeek}
255
- playerState={playerState}
256
- startComponentsAnimation={startComponentsAnimation}
257
- docked
258
- />
259
- )}
248
+ (layoutState.docked || startComponentsAnimation) ? (
249
+ <ProgressBar
250
+ name={"audio-docked-layout"}
251
+ content={entry}
252
+ value={value}
253
+ visible={visible}
254
+ layoutState={layoutState}
255
+ onPlayerSeek={onPlayerSeek}
256
+ playerState={playerState}
257
+ startComponentsAnimation={startComponentsAnimation}
258
+ docked
259
+ />
260
+ ) : null}
260
261
 
261
262
  <View style={styles.content}>
262
263
  <View
@@ -17,7 +17,7 @@ const playbackButtonMargins = (
17
17
  isTablet: boolean,
18
18
  isLiveShowFullControls: boolean
19
19
  ): number =>
20
- isLiveShowFullControls ? (isTablet ? 65 : 10) : isTablet ? 95 : 40;
20
+ isLiveShowFullControls ? (isTablet ? 65 : 10) : isTablet ? undefined : 40;
21
21
 
22
22
  export const PADDING_HORIZONTAL = 15;
23
23
 
@@ -37,8 +37,8 @@ export const playbackButton = ({
37
37
  const icon = playerState.isPaused
38
38
  ? "play"
39
39
  : playerState.isLive && liveButton
40
- ? "stop"
41
- : "pause";
40
+ ? "stop"
41
+ : "pause";
42
42
 
43
43
  return {
44
44
  size: playbackButtonSize(isTablet),
@@ -47,7 +47,7 @@ const fetchPaletteAndDominantColor = async (imageUrl: string) => {
47
47
  const dominant = await getDominantColor(imageUrl);
48
48
  const dominantColor = `rgb(${dominant.r}, ${dominant.g}, ${dominant.b})`;
49
49
 
50
- return { gradientColors: colors.slice(0, 2), dominantColor };
50
+ return { gradientColors: colors.slice(0, 2).reverse(), dominantColor };
51
51
  };
52
52
 
53
53
  export const MagicBG: React.FC<MagicBGProps> = ({
@@ -0,0 +1,119 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
3
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
4
+ import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
5
+ import { hasDatePassed, NAMESPACE } from "./utils";
6
+
7
+ const { log_debug, log_error } = createLogger({
8
+ subsystem: "Player",
9
+ category: "SleepTimerManager",
10
+ });
11
+
12
+ export type SleepTimer = {
13
+ label: string;
14
+ // interval in minutes for UI at the moment when timer was first started
15
+ value: number;
16
+ };
17
+
18
+ export const SLEEP_TIMER_OFF: SleepTimer = {
19
+ label: "playback_sleep_off_label",
20
+ value: 0,
21
+ };
22
+
23
+ export class SleepTimerState {
24
+ constructor(
25
+ public timer: SleepTimer,
26
+ public timerDate: number
27
+ ) {}
28
+ }
29
+
30
+ const SLEEP_PREFERENCE_KEY = "sleep";
31
+
32
+ export class SleepTimerController {
33
+ private sleepTimerStateSubject = new BehaviorSubject<SleepTimerState>(
34
+ new SleepTimerState(SLEEP_TIMER_OFF, 0)
35
+ );
36
+
37
+ constructor(private player: Player) {}
38
+
39
+ get sleepTimerState$() {
40
+ return this.sleepTimerStateSubject.asObservable();
41
+ }
42
+
43
+ private async scheduleSleep(sleepDate): Promise<boolean> {
44
+ log_debug(`Setting sleep interval to ${sleepDate}`);
45
+
46
+ try {
47
+ this.player.cancelSleepTimer?.();
48
+ this.player.startSleepTimer?.(sleepDate);
49
+
50
+ return true;
51
+ } catch (e) {
52
+ log_error("Failed to start sleep timer", { error: e.message });
53
+
54
+ return false;
55
+ }
56
+ }
57
+
58
+ async setSleepTimer(item: SleepTimer) {
59
+ // User has selected Off state
60
+ if (item.value === 0) {
61
+ log_debug("Cancelling sleep timer");
62
+ this.player.cancelSleepTimer?.();
63
+
64
+ return this.onTimerExpired();
65
+ }
66
+
67
+ log_debug(`Starting sleep timer in ${item.value} minutes`);
68
+
69
+ const futureSleepDate = Date.now() + item.value * 60000;
70
+
71
+ if (await this.scheduleSleep(futureSleepDate)) {
72
+ await localStorage.setItem(
73
+ SLEEP_PREFERENCE_KEY,
74
+ { ...item, futureSleepDate },
75
+ NAMESPACE
76
+ );
77
+
78
+ this.sleepTimerStateSubject.next(
79
+ new SleepTimerState(item, futureSleepDate)
80
+ );
81
+ }
82
+ }
83
+
84
+ async onTimerExpired() {
85
+ await localStorage.removeItem(SLEEP_PREFERENCE_KEY, NAMESPACE);
86
+
87
+ this.sleepTimerStateSubject.next(new SleepTimerState(SLEEP_TIMER_OFF, 0));
88
+ }
89
+
90
+ async loadSleep() {
91
+ const sleep = await localStorage.getItem(SLEEP_PREFERENCE_KEY, NAMESPACE);
92
+
93
+ if (!sleep) return;
94
+
95
+ const { futureSleepDate, ...rest } = sleep;
96
+
97
+ if (!futureSleepDate || hasDatePassed(futureSleepDate)) {
98
+ log_debug("Sleep date has already passed");
99
+ await localStorage.removeItem(SLEEP_PREFERENCE_KEY, NAMESPACE);
100
+
101
+ return;
102
+ }
103
+
104
+ if (await this.scheduleSleep(futureSleepDate)) {
105
+ log_debug(`Sleep state restored, will go to sleep at ${futureSleepDate}`);
106
+
107
+ this.sleepTimerStateSubject.next(
108
+ new SleepTimerState(rest as SleepTimer, futureSleepDate)
109
+ );
110
+ }
111
+ }
112
+
113
+ // Will be called if user closed the player explicitly.
114
+ // TODO: check if it will be called on Android when we need.
115
+ // Probably we will have to handle it on native instead.
116
+ public static async onPlayerDestroy() {
117
+ await localStorage.removeItem(SLEEP_PREFERENCE_KEY, NAMESPACE);
118
+ }
119
+ }
@@ -0,0 +1,52 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
3
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
4
+ import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
5
+ import { NAMESPACE } from "./utils";
6
+
7
+ const { log_debug } = createLogger({
8
+ subsystem: "Player",
9
+ category: "PlaybackSpeedManager",
10
+ });
11
+
12
+ export type PlaybackSpeed = {
13
+ asset: string;
14
+ value: number;
15
+ };
16
+
17
+ const SPEED_PREFERENCE_KEY = "speed";
18
+
19
+ export const DEFAULT_SPEED: PlaybackSpeed = {
20
+ asset: "speed_1",
21
+ value: 1.0,
22
+ };
23
+
24
+ // todo: should not be applied to live streams
25
+ export class SpeedController {
26
+ private playbackSpeedSubject = new BehaviorSubject<PlaybackSpeed>(
27
+ DEFAULT_SPEED
28
+ );
29
+
30
+ constructor(private player: Player) {}
31
+
32
+ get playbackSpeed$() {
33
+ return this.playbackSpeedSubject.asObservable();
34
+ }
35
+
36
+ async loadSpeed() {
37
+ const speed = await localStorage.getItem(SPEED_PREFERENCE_KEY, NAMESPACE);
38
+
39
+ // checking for value in case we will pull empty or broken object
40
+ if (speed && speed.value) {
41
+ this.playbackSpeedSubject.next(speed);
42
+ this.player.setPlaybackRate?.(speed.value);
43
+ log_debug(`Speed loaded: ${speed}`);
44
+ }
45
+ }
46
+
47
+ async setPlaybackSpeed(speed: PlaybackSpeed) {
48
+ await localStorage.setItem(SPEED_PREFERENCE_KEY, speed, NAMESPACE);
49
+ this.playbackSpeedSubject.next(speed);
50
+ this.player.setPlaybackRate?.(speed.value);
51
+ }
52
+ }
@@ -1,4 +1,8 @@
1
1
  import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
2
+ import {
3
+ PlayerAnimationStateEnum,
4
+ PlayerAnimationStateT,
5
+ } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation/ModalAnimationContext";
2
6
  import { parseJsonIfNeeded } from "@applicaster/zapp-react-native-utils/functionUtils";
3
7
 
4
8
  export const NAMESPACE = "audio-player";
@@ -31,3 +35,23 @@ export const formatSpeed = (value: number) => {
31
35
  // Return the formatted string with "speed_" prefix
32
36
  return `speed_${formattedValue}`;
33
37
  };
38
+
39
+ export const calculateBorderRadius = (
40
+ isTablet: boolean,
41
+ docked: boolean,
42
+ startComponentsAnimation: boolean,
43
+ isActiveGesture: boolean,
44
+ playerAnimationState: PlayerAnimationStateT
45
+ ): number => {
46
+ const shouldRemoveBorderRadius =
47
+ (startComponentsAnimation &&
48
+ isActiveGesture &&
49
+ playerAnimationState !== PlayerAnimationStateEnum.maximize) ||
50
+ docked;
51
+
52
+ if (shouldRemoveBorderRadius) {
53
+ return 0;
54
+ }
55
+
56
+ return isTablet ? 24 : 10;
57
+ };
@@ -253,12 +253,7 @@ export default class VideoPlayer extends React.Component<
253
253
  };
254
254
 
255
255
  stopBackgroundPlayback = () => {
256
- if (
257
- this.isReactNativeMethodsSupported() &&
258
- RNPlayerManager?.[this.playerPluginId]?.stopBackgroundPlayback
259
- ) {
260
- RNPlayerManager?.[this.playerPluginId]?.stopBackgroundPlayback();
261
- }
256
+ RNPlayerManager?.[this.playerPluginId]?.stopBackgroundPlayback?.();
262
257
  };
263
258
 
264
259
  forward = (seekTime = SEEK_TIME) => {
@@ -726,7 +721,7 @@ export default class VideoPlayer extends React.Component<
726
721
 
727
722
  const isAudioItem =
728
723
  entry?.content?.type?.includes?.("audio") ||
729
- entry?.type?.value === "audio";
724
+ entry?.type?.value?.includes?.("audio");
730
725
 
731
726
  const getStyle = (dimensions) => {
732
727
  const generalStyles = isAudioItem ? styles.audio : styles.video;
@@ -811,7 +806,6 @@ export default class VideoPlayer extends React.Component<
811
806
  nowPlayingEnabled,
812
807
  paused,
813
808
  seek,
814
- rate,
815
809
  textTrackId,
816
810
  liveSeekingEnabled,
817
811
  liveCatchUpEnabled,
@@ -822,7 +816,14 @@ export default class VideoPlayer extends React.Component<
822
816
  accessibilityProps: getAllAccessibilityProps(
823
817
  this.getPluginConfiguration()
824
818
  ),
825
- }; // rate temporary removed, because of bug with subtitles
819
+ };
820
+
821
+ if (!isAppleTV()) {
822
+ // TODO: Move to RN method and remove from props.
823
+ // Causes tvOS bug: native player passes rate on fast seek,
824
+ // RN returns it and it breaks playback.
825
+ nativeProps.rate = rate;
826
+ }
826
827
 
827
828
  if (
828
829
  this.isReactNativeMethodsSupported() &&
@@ -923,8 +924,8 @@ export default class VideoPlayer extends React.Component<
923
924
  const playerContainerStyles = isApplePlatform()
924
925
  ? styles.playerContainerBlack
925
926
  : isTV() && isAudioItem
926
- ? styles.playerContainerTransparent
927
- : styles.playerContainerBlack;
927
+ ? styles.playerContainerTransparent
928
+ : styles.playerContainerBlack;
928
929
 
929
930
  const isInline = () => {
930
931
  if (isTV()) {
@@ -995,14 +996,14 @@ export default class VideoPlayer extends React.Component<
995
996
  const renderMobilePlayer = () => {
996
997
  return isFullScreenAudioPlayer && isAudioItem ? (
997
998
  <View style={containerStyle}>
998
- {(!castAction?.state?.connected || supportNativeCast) && (
999
+ {!castAction?.state?.connected || supportNativeCast ? (
999
1000
  <this.props.PlayerComponent
1000
1001
  ref={this._assignRoot}
1001
1002
  {...nativeEvents}
1002
1003
  {...nativeProps}
1003
1004
  listener={this.props.listener}
1004
1005
  />
1005
- )}
1006
+ ) : null}
1006
1007
  <AudioPlayerWrapper {...sharedWrapperViewProps} />
1007
1008
  </View>
1008
1009
  ) : (
@@ -1017,7 +1018,7 @@ export default class VideoPlayer extends React.Component<
1017
1018
  return (
1018
1019
  <View style={styles.container}>
1019
1020
  <View style={[playerContainerStyles, playerDimensions]}>
1020
- {isAudioItem && (
1021
+ {isAudioItem ? (
1021
1022
  <AnimationComponent
1022
1023
  animationType={ComponentAnimationType.player}
1023
1024
  additionalData={{
@@ -1036,18 +1037,18 @@ export default class VideoPlayer extends React.Component<
1036
1037
  plugin_configuration={this.getPluginConfiguration()}
1037
1038
  />
1038
1039
  </AnimationComponent>
1039
- )}
1040
+ ) : null}
1040
1041
 
1041
- {shouldRenderBackground && (
1042
+ {shouldRenderBackground ? (
1042
1043
  <PlayerImageBackground
1043
1044
  imageKey={backgroundImageKey}
1044
1045
  configuration={this.getPluginConfiguration()}
1045
1046
  entry={entry}
1046
1047
  style={getBackgroundImageStyle(playerDimensions)}
1047
1048
  />
1048
- )}
1049
+ ) : null}
1049
1050
 
1050
- {(!castAction?.state?.connected || supportNativeCast) && (
1051
+ {!castAction?.state?.connected || supportNativeCast ? (
1051
1052
  <AnimationComponent
1052
1053
  animationType={ComponentAnimationType.player}
1053
1054
  additionalData={playerDimensions}
@@ -1069,7 +1070,7 @@ export default class VideoPlayer extends React.Component<
1069
1070
  {this.childrenWithPlayNext()}
1070
1071
  </this.props.PlayerComponent>
1071
1072
  </AnimationComponent>
1072
- )}
1073
+ ) : null}
1073
1074
  </View>
1074
1075
 
1075
1076
  {renderTransportControls(playerDimensions)}