@applicaster/quick-brick-player 13.0.0-alpha.3013227535 → 13.0.0-alpha.3099523645

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-alpha.3013227535",
3
+ "version": "13.0.0-alpha.3099523645",
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.25",
40
- "@applicaster/quick-brick-tv-transport-controls": "13.0.0-rc.26",
41
- "@applicaster/zapp-react-native-tvos-app": "13.0.0-alpha.3013227535",
42
- "@applicaster/zapp-react-native-ui-components": "13.0.0-alpha.3013227535",
43
- "@applicaster/zapp-react-native-utils": "13.0.0-alpha.3013227535",
39
+ "@applicaster/quick-brick-mobile-transport-controls": "13.0.0-rc.46",
40
+ "@applicaster/quick-brick-tv-transport-controls": "13.0.0-rc.46",
41
+ "@applicaster/zapp-react-native-tvos-app": "13.0.0-alpha.3099523645",
42
+ "@applicaster/zapp-react-native-ui-components": "13.0.0-alpha.3099523645",
43
+ "@applicaster/zapp-react-native-utils": "13.0.0-alpha.3099523645",
44
44
  "keymirror": "^0.1.1",
45
45
  "query-string": "7.1.3",
46
46
  "react-native-web-linear-gradient": "1.1.2",
@@ -64,9 +64,9 @@
64
64
  "@types/node": "*",
65
65
  "@types/react-native": "0.69.6",
66
66
  "immer": "*",
67
- "react": "17.0.2",
67
+ "react": "*",
68
68
  "react-hot-loader": "*",
69
- "react-native": "0.68.6",
69
+ "react-native": "*",
70
70
  "react-native-fs": "*",
71
71
  "react-native-gesture-handler": "*",
72
72
  "react-native-linear-gradient": "*",
@@ -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";
@@ -15,15 +15,9 @@ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
15
15
  import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
16
16
  import { PlayerDetails } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/PlayerDetails";
17
17
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
18
- import { useCallback, useEffect, useState } from "react";
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,20 @@ 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";
36
44
 
37
45
  const { log_debug } = createLogger({
38
46
  subsystem: "UIComponent",
@@ -71,8 +79,7 @@ type Props = {
71
79
  isTabletPortrait?: boolean;
72
80
  };
73
81
 
74
- export const defaultAspectRatioWidth = (height: number): number =>
75
- (height / 9) * 16;
82
+ const { width: SCREEN_WIDTH } = ScreenDimensions.get("screen");
76
83
 
77
84
  const Styles = {
78
85
  mainContainer: (style: ViewStyle) => {
@@ -98,7 +105,11 @@ const Styles = {
98
105
  height: 96,
99
106
  marginHorizontal: 24,
100
107
  },
101
- playerDetails: { marginTop: 60 },
108
+ playerDetails: {
109
+ marginTop: 60,
110
+ alignSelf: "center",
111
+ width: SCREEN_WIDTH - PADDING_HORIZONTAL * 2,
112
+ },
102
113
 
103
114
  linearGradient: {
104
115
  flex: 1,
@@ -132,11 +143,11 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
132
143
  } = props;
133
144
 
134
145
  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 },
146
+ { asset: configuration["speed_0_8"] || speed_0_8, value: 0.8 },
147
+ { asset: configuration["speed_1"] || speed_1, value: 1.0 },
148
+ { asset: configuration["speed_1_2"] || speed_1_2, value: 1.2 },
149
+ { asset: configuration["speed_1_5"] || speed_1_5, value: 1.5 },
150
+ { asset: configuration["speed_2"] || speed_2, value: 2.0 },
140
151
  ];
141
152
 
142
153
  const isTablet = useIsTablet();
@@ -154,17 +165,29 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
154
165
  configuration?.audio_live_player_update_interval
155
166
  );
156
167
 
157
- const [playbackSpeed, setPlaybackSpeed] = useState({
158
- asset: configuration["speed_1"],
159
- value: 1.0,
160
- });
168
+ const [sleepTimerManager, setSleepTimerManager] =
169
+ useState<SleepTimerController | null>(null);
161
170
 
162
- const [sleepTimer, setSleepTimer] = useState({
163
- label: "playback_sleep_off_label",
164
- value: 0,
165
- });
171
+ const [speedManager, setSpeedManager] = useState<SpeedController | null>(
172
+ null
173
+ );
174
+
175
+ useEffect(() => {
176
+ if (player) {
177
+ setSleepTimerManager(new SleepTimerController(player));
178
+ setSpeedManager(new SpeedController(player));
179
+ } else {
180
+ setSleepTimerManager(null);
181
+ setSpeedManager(null);
182
+ }
183
+ }, [player]);
184
+
185
+ const [playbackSpeed, setPlaybackSpeed] =
186
+ useState<PlaybackSpeed>(DEFAULT_SPEED);
166
187
 
167
- const [sleepTimerDate, setSleepTimerDate] = useState(0);
188
+ const [sleepTimerState, setSleepTimerState] = useState<SleepTimerState>(
189
+ new SleepTimerState(SLEEP_TIMER_OFF, 0)
190
+ );
168
191
 
169
192
  const [defaultDimensions, setDefaultDimensions] =
170
193
  React.useState<Dimensions>(undefined);
@@ -189,90 +212,27 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
189
212
  if (!defaultDimensions) setDefaultDimensions({ width, height });
190
213
  };
191
214
 
192
- const cancelSleepTimer = useCallback(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
- }, [player, setSleepTimer, setSleepTimerDate]);
204
-
205
- const startSleepTimer = useCallback(
206
- (timerValue: number) => {
207
- try {
208
- const futureSleepDate = Date.now() + timerValue * 60000;
209
-
210
- player?.cancelSleepTimer?.();
211
-
212
- player?.startSleepTimer?.(futureSleepDate);
213
-
214
- return futureSleepDate;
215
- } catch (e) {
216
- return 0;
217
- }
218
- },
219
- [player]
220
- );
221
-
222
- const handleSleep = useCallback(
223
- async (timerValue: number, currentFutureSleepDate = null) => {
224
- if (currentFutureSleepDate) {
225
- if (hasDatePassed(currentFutureSleepDate)) {
226
- await cancelSleepTimer();
227
-
228
- return 0;
229
- }
230
-
231
- return currentFutureSleepDate;
232
- }
233
-
234
- if (timerValue === 0) {
235
- await cancelSleepTimer();
236
-
237
- return timerValue;
238
- }
239
-
240
- return startSleepTimer(timerValue);
241
- },
242
- [startSleepTimer, cancelSleepTimer]
243
- );
244
-
245
215
  useEffect(() => {
246
- const loadSpeed = async () => {
247
- const speed = await loadItemsFromStorage("speed");
216
+ const sleepSubscription =
217
+ sleepTimerManager?.sleepTimerState$.subscribe(setSleepTimerState);
248
218
 
249
- if (playerState.isReadyToPlay && speed) {
250
- setPlaybackSpeed(speed);
251
- player?.setPlaybackRate(speed.value);
252
- }
253
- };
254
-
255
- const loadSleep = async () => {
256
- const sleep = await loadItemsFromStorage("sleep");
219
+ const speedSubscription =
220
+ speedManager?.playbackSpeed$.subscribe(setPlaybackSpeed);
257
221
 
258
- if (playerState.isReadyToPlay && sleep) {
259
- const { futureSleepDate, ...rest } = sleep;
260
- setSleepTimer(rest);
261
- const sleepTimerDate = await handleSleep(sleep.value, futureSleepDate);
262
- setSleepTimerDate(sleepTimerDate);
263
- }
222
+ return () => {
223
+ sleepSubscription?.unsubscribe();
224
+ speedSubscription?.unsubscribe();
264
225
  };
226
+ }, [sleepTimerManager, speedManager]);
265
227
 
266
- void loadSpeed();
267
- void loadSleep();
268
- }, [
269
- playerState.isReadyToPlay,
270
- handleSleep,
271
- setSleepTimer,
272
- setSleepTimerDate,
273
- setPlaybackSpeed,
274
- player,
275
- ]);
228
+ useEffect(() => {
229
+ if (!playerState.isReadyToPlay) {
230
+ return;
231
+ }
232
+
233
+ void speedManager?.loadSpeed();
234
+ void sleepTimerManager?.loadSleep();
235
+ }, [playerState.isReadyToPlay, speedManager, sleepTimerManager]);
276
236
 
277
237
  const getValue = (key: string, stylesObj: {}) =>
278
238
  R.propOr(null, key, stylesObj);
@@ -315,26 +275,16 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
315
275
 
316
276
  const onSpeedSelect = React.useCallback(
317
277
  async (item) => {
318
- setPlaybackSpeed(item);
319
- player?.setPlaybackRate(item.value);
320
- await localStorage.setItem("speed", item, NAMESPACE);
278
+ await speedManager?.setPlaybackSpeed(item);
321
279
  },
322
- [player, setPlaybackSpeed]
280
+ [speedManager]
323
281
  );
324
282
 
325
283
  const onSleepSelect = React.useCallback(
326
284
  async (item) => {
327
- setSleepTimer(item);
328
- const futureSleepDate = await handleSleep(item.value);
329
- setSleepTimerDate(futureSleepDate);
330
-
331
- await localStorage.setItem(
332
- "sleep",
333
- { ...item, futureSleepDate },
334
- NAMESPACE
335
- );
285
+ await sleepTimerManager?.setSleepTimer(item);
336
286
  },
337
- [setSleepTimer, setSleepTimerDate, handleSleep]
287
+ [sleepTimerManager]
338
288
  );
339
289
 
340
290
  const onSpeedButtonPress = React.useCallback(() => {
@@ -361,9 +311,12 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
361
311
  navigator.openModalBottomSheet({
362
312
  modalBottomSheetContentProps: {
363
313
  items: [
364
- { label: configuration["playback_sleep_off_label"], value: 0 },
314
+ {
315
+ label: configuration["playback_sleep_off_label"],
316
+ value: 0,
317
+ },
365
318
  ].concat(sleeps),
366
- current_selection: sleepTimer,
319
+ current_selection: sleepTimerState.timer,
367
320
  onPress: onSleepSelect,
368
321
  title: configuration["playback_sleep_title"],
369
322
  },
@@ -373,12 +326,20 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
373
326
  });
374
327
  }, [
375
328
  navigator,
376
- sleepTimer,
329
+ sleepTimerState.timer,
377
330
  configuration["playback_sleep_title"],
378
331
  configuration["playback_sleep_off_label"],
379
332
  onSleepSelect,
380
333
  ]);
381
334
 
335
+ // This method does not actually cancel the sleep timer
336
+ // (and should not to avoid race condition),
337
+ // it just performs the cleanup when timer expires
338
+ // TODO: rename it in code above
339
+ const cancelSleepTimer = React.useCallback(() => {
340
+ sleepTimerManager?.onTimerExpired();
341
+ }, [sleepTimerManager]);
342
+
382
343
  const onCloseVideoModal = React.useCallback(() => {
383
344
  playerManager.close();
384
345
  }, [playerManager]);
@@ -426,9 +387,11 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
426
387
  onSleepButtonPress={onSleepButtonPress}
427
388
  playbackSpeedName={formatSpeed(playbackSpeed.value)}
428
389
  playbackSleepName={
429
- sleepTimer.value === 0 ? "sleep_timer" : "sleep_timer_active"
390
+ sleepTimerState.timer.value === 0
391
+ ? "sleep_timer"
392
+ : "sleep_timer_active"
430
393
  }
431
- sleepTimerDate={sleepTimerDate}
394
+ sleepTimerDate={sleepTimerState.timerDate}
432
395
  cancelSleepTimer={cancelSleepTimer}
433
396
  onSkipLive={onSkipLive}
434
397
  onCloseVideoModal={onCloseVideoModal}
@@ -478,12 +441,12 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
478
441
  playNextData,
479
442
  onSkipStart,
480
443
  onSkipLive,
481
- sleepTimer.value,
482
- sleepTimerDate,
444
+ sleepTimerState.timer.value,
445
+ sleepTimerState.timerDate,
483
446
  ]);
484
447
 
485
448
  if (configuration.magic_background) {
486
- const baseUrl = entry?.extensions?.audio_player_background_image ?? "";
449
+ const baseUrl = imageSrcFromMediaItem(entry, null);
487
450
 
488
451
  const queryParamName =
489
452
  configuration.audio_player_background_image_query || "width";
@@ -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
4
  import { View } 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,23 +16,22 @@ 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
36
  import DockedLayout from "./DockedLayout";
37
37
  import { isValidDate } from "../../Utils";
@@ -375,6 +375,7 @@ export default function DefaultLayout(props: LayoutProps) {
375
375
  >
376
376
  <PlayerImageBackground
377
377
  entry={entry}
378
+ docked={docked}
378
379
  style={Styles.backgroundImageContainer(dimensions, isTablet)}
379
380
  imageStyle={Styles.backgroundImageContainerImageStyle(
380
381
  isTablet,
@@ -405,7 +406,7 @@ export default function DefaultLayout(props: LayoutProps) {
405
406
  layoutState={layoutState}
406
407
  />
407
408
  <Separator />
408
- {needsToShowProgressBar && (
409
+ {needsToShowProgressBar ? (
409
410
  <View>
410
411
  <ProgressBar
411
412
  content={entry}
@@ -422,17 +423,17 @@ export default function DefaultLayout(props: LayoutProps) {
422
423
  onSkipLive={onSkipLive}
423
424
  />
424
425
 
425
- {showCounter && timeLeft >= 0 && (
426
+ {showCounter && timeLeft >= 0 ? (
426
427
  <CountDown
427
428
  timeLeft={timeLeft}
428
429
  value={value}
429
430
  title={sleepTitle}
430
431
  />
431
- )}
432
+ ) : null}
432
433
 
433
434
  <Separator />
434
435
  </View>
435
- )}
436
+ ) : null}
436
437
  <Controls
437
438
  value={value}
438
439
  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 = {
@@ -245,18 +245,18 @@ 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
+ content={entry}
251
+ value={value}
252
+ visible={visible}
253
+ layoutState={layoutState}
254
+ onPlayerSeek={onPlayerSeek}
255
+ playerState={playerState}
256
+ startComponentsAnimation={startComponentsAnimation}
257
+ docked
258
+ />
259
+ ) : null}
260
260
 
261
261
  <View style={styles.content}>
262
262
  <View
@@ -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
+ }
@@ -806,7 +806,6 @@ export default class VideoPlayer extends React.Component<
806
806
  nowPlayingEnabled,
807
807
  paused,
808
808
  seek,
809
- rate,
810
809
  textTrackId,
811
810
  liveSeekingEnabled,
812
811
  liveCatchUpEnabled,
@@ -817,7 +816,14 @@ export default class VideoPlayer extends React.Component<
817
816
  accessibilityProps: getAllAccessibilityProps(
818
817
  this.getPluginConfiguration()
819
818
  ),
820
- }; // 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
+ }
821
827
 
822
828
  if (
823
829
  this.isReactNativeMethodsSupported() &&
@@ -990,14 +996,14 @@ export default class VideoPlayer extends React.Component<
990
996
  const renderMobilePlayer = () => {
991
997
  return isFullScreenAudioPlayer && isAudioItem ? (
992
998
  <View style={containerStyle}>
993
- {(!castAction?.state?.connected || supportNativeCast) && (
999
+ {!castAction?.state?.connected || supportNativeCast ? (
994
1000
  <this.props.PlayerComponent
995
1001
  ref={this._assignRoot}
996
1002
  {...nativeEvents}
997
1003
  {...nativeProps}
998
1004
  listener={this.props.listener}
999
1005
  />
1000
- )}
1006
+ ) : null}
1001
1007
  <AudioPlayerWrapper {...sharedWrapperViewProps} />
1002
1008
  </View>
1003
1009
  ) : (
@@ -1012,7 +1018,7 @@ export default class VideoPlayer extends React.Component<
1012
1018
  return (
1013
1019
  <View style={styles.container}>
1014
1020
  <View style={[playerContainerStyles, playerDimensions]}>
1015
- {isAudioItem && (
1021
+ {isAudioItem ? (
1016
1022
  <AnimationComponent
1017
1023
  animationType={ComponentAnimationType.player}
1018
1024
  additionalData={{
@@ -1031,18 +1037,18 @@ export default class VideoPlayer extends React.Component<
1031
1037
  plugin_configuration={this.getPluginConfiguration()}
1032
1038
  />
1033
1039
  </AnimationComponent>
1034
- )}
1040
+ ) : null}
1035
1041
 
1036
- {shouldRenderBackground && (
1042
+ {shouldRenderBackground ? (
1037
1043
  <PlayerImageBackground
1038
1044
  imageKey={backgroundImageKey}
1039
1045
  configuration={this.getPluginConfiguration()}
1040
1046
  entry={entry}
1041
1047
  style={getBackgroundImageStyle(playerDimensions)}
1042
1048
  />
1043
- )}
1049
+ ) : null}
1044
1050
 
1045
- {(!castAction?.state?.connected || supportNativeCast) && (
1051
+ {!castAction?.state?.connected || supportNativeCast ? (
1046
1052
  <AnimationComponent
1047
1053
  animationType={ComponentAnimationType.player}
1048
1054
  additionalData={playerDimensions}
@@ -1064,7 +1070,7 @@ export default class VideoPlayer extends React.Component<
1064
1070
  {this.childrenWithPlayNext()}
1065
1071
  </this.props.PlayerComponent>
1066
1072
  </AnimationComponent>
1067
- )}
1073
+ ) : null}
1068
1074
  </View>
1069
1075
 
1070
1076
  {renderTransportControls(playerDimensions)}