@applicaster/quick-brick-player 12.0.0-rc.4 → 12.0.0-rc.41

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": "12.0.0-rc.4",
3
+ "version": "12.0.0-rc.41",
4
4
  "description": "Quick Brick Player",
5
5
  "main": "./src/index.ts",
6
6
  "types": "index.d.ts",
@@ -36,12 +36,13 @@
36
36
  },
37
37
  "homepage": "https://github.com/applicaster/quickbrick#readme",
38
38
  "dependencies": {
39
- "@applicaster/quick-brick-mobile-transport-controls": "12.0.0-rc.4",
39
+ "@applicaster/quick-brick-mobile-transport-controls": "12.0.0-rc.33",
40
40
  "@applicaster/quick-brick-tv-transport-controls": "11.0.0-rc.133",
41
- "@applicaster/zapp-react-native-tvos-app": "12.0.0-rc.4",
42
- "@applicaster/zapp-react-native-ui-components": "12.0.0-rc.4",
43
- "@applicaster/zapp-react-native-utils": "12.0.0-rc.4",
41
+ "@applicaster/zapp-react-native-tvos-app": "12.0.0-rc.41",
42
+ "@applicaster/zapp-react-native-ui-components": "12.0.0-rc.41",
43
+ "@applicaster/zapp-react-native-utils": "12.0.0-rc.41",
44
44
  "keymirror": "^0.1.1",
45
+ "query-string": "7.1.3",
45
46
  "react-native-web-linear-gradient": "1.1.2",
46
47
  "shaka-player": "4.3.5",
47
48
  "typeface-montserrat": "^0.0.54",
@@ -1,8 +1,9 @@
1
1
  import React, { useEffect, useState } from "react";
2
- import { Image, StyleSheet, View } from "react-native";
2
+ import { StyleSheet, View } from "react-native";
3
3
  import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
4
4
  import { MUTED_USAGE } from "../const";
5
5
  import { LiveImageManager } from "@applicaster/zapp-react-native-ui-components/Components/VideoLive/LiveImageManager";
6
+ import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
6
7
 
7
8
  type Asset = string | undefined;
8
9
 
@@ -3,7 +3,8 @@ import { Text, TextStyle, View } from "react-native";
3
3
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
4
4
 
5
5
  type AudioDescriptionProps = {
6
- content: ZappEntry;
6
+ title: string | number;
7
+ summary: string | number;
7
8
  layoutState: {
8
9
  inline: boolean;
9
10
  docked: boolean;
@@ -78,8 +79,7 @@ const summaryStyles = (value: GetValue, prefix: string): TextStyle => ({
78
79
  });
79
80
 
80
81
  export const AudioDescription = (props: AudioDescriptionProps) => {
81
- const { content, layoutState, value } = props;
82
- const { title, summary } = content;
82
+ const { title, summary, layoutState, value } = props;
83
83
  const prefix = layoutState.docked ? "docked_player_" : "";
84
84
 
85
85
  return (
@@ -15,7 +15,17 @@ 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 { useEffect, useState } from "react";
18
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";
27
+ import { addQueryParam, getLiveUrl } from "../Utils";
28
+ import { useTitleSummaryOverlay } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerTitleSummaryOverlay";
19
29
 
20
30
  const { log_debug } = createLogger({
21
31
  subsystem: "UIComponent",
@@ -32,6 +42,7 @@ type Dimensions = {
32
42
  type Configuration = {
33
43
  [key: string]: any;
34
44
  magic_background?: boolean;
45
+ audio_player_background_image_query?: string;
35
46
  tablet_landscape_sidebar_width?: string;
36
47
  tablet_landscape_player_container_background_color?: string;
37
48
  };
@@ -96,6 +107,17 @@ const Styles = {
96
107
  },
97
108
  };
98
109
 
110
+ const sleeps = [
111
+ // { label: "End of episode", value: "episode" },
112
+ { label: "5 minutes", value: 5 },
113
+ { label: "10 minutes", value: 10 },
114
+ { label: "15 minutes", value: 15 },
115
+ { label: "30 minutes", value: 30 },
116
+ { label: "45 minutes", value: 45 },
117
+ { label: "1 hour", value: 60 },
118
+ { label: "2 hours", value: 120 },
119
+ ];
120
+
99
121
  export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
100
122
  const player = usePlayer();
101
123
  const playerState = usePlayerState("audio-player-wrapper");
@@ -111,15 +133,48 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
111
133
  isTabletPortrait,
112
134
  } = props;
113
135
 
136
+ const speeds = [
137
+ { asset: configuration["speed_0_8"], value: 0.8 },
138
+ { asset: configuration["speed_1"], value: 1.0 },
139
+ { asset: configuration["speed_1_2"], value: 1.2 },
140
+ { asset: configuration["speed_1_5"], value: 1.5 },
141
+ { asset: configuration["speed_2"], value: 2.0 },
142
+ ];
143
+
114
144
  const isTablet = useIsTablet();
115
145
 
116
146
  const isTabletLandscape = !isTV() && isTablet && !isTabletPortrait;
117
147
 
148
+ // TODO: Have generic place for live url from configuration. Currently its hardcoded for client.
149
+
150
+ const feedUrl = getLiveUrl(entry?.extensions?.live?.channel_src ?? "");
151
+
152
+ const { title, summary } = useTitleSummaryOverlay(
153
+ feedUrl,
154
+ entry,
155
+ playerState?.isLive,
156
+ configuration?.audio_live_player_update_interval
157
+ );
158
+
159
+ const [playbackSpeed, setPlaybackSpeed] = useState({
160
+ asset: configuration["speed_1"],
161
+ value: 1.0,
162
+ });
163
+
164
+ const [sleepTimer, setSleepTimer] = useState({
165
+ label: "playback_sleep_off_label",
166
+ value: 0,
167
+ });
168
+
169
+ const [sleepTimerDate, setSleepTimerDate] = useState(0);
170
+
118
171
  const [dimensions, setDimensions] = React.useState<Dimensions>({
119
172
  width: 0,
120
173
  height: 0,
121
174
  });
122
175
 
176
+ const navigator = useNavigation();
177
+
123
178
  const { maximiseVideoModal } = useNavigation();
124
179
 
125
180
  const onLayout = (event: any) => {
@@ -131,6 +186,83 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
131
186
  });
132
187
  };
133
188
 
189
+ const cancelSleepTimer = async () => {
190
+ player?.cancelSleepTimer();
191
+
192
+ setSleepTimer({
193
+ label: "playback_sleep_off_label",
194
+ value: 0,
195
+ });
196
+
197
+ setSleepTimerDate(0);
198
+
199
+ await localStorage.removeItem("sleep", NAMESPACE);
200
+ };
201
+
202
+ const startSleepTimer = (timerValue: number) => {
203
+ try {
204
+ const currentDate = new Date();
205
+ const futureDate = new Date(currentDate.getTime() + timerValue * 60000);
206
+ const futureSleepDate = futureDate.getTime();
207
+
208
+ player?.cancelSleepTimer();
209
+
210
+ player?.startSleepTimer(futureSleepDate);
211
+
212
+ return futureSleepDate;
213
+ } catch (e) {
214
+ return 0;
215
+ }
216
+ };
217
+
218
+ const handleSleep = async (
219
+ timerValue: number,
220
+ currentFutureSleepDate = null
221
+ ) => {
222
+ if (currentFutureSleepDate) {
223
+ if (hasDatePassed(currentFutureSleepDate)) {
224
+ await cancelSleepTimer();
225
+
226
+ return 0;
227
+ }
228
+
229
+ return currentFutureSleepDate;
230
+ }
231
+
232
+ if (timerValue === 0) {
233
+ await cancelSleepTimer();
234
+
235
+ return timerValue;
236
+ }
237
+
238
+ return startSleepTimer(timerValue);
239
+ };
240
+
241
+ useEffect(() => {
242
+ const loadSpeed = async () => {
243
+ const speed = await loadItemsFromStorage("speed");
244
+
245
+ if (playerState.isReadyToPlay && speed) {
246
+ setPlaybackSpeed(speed);
247
+ player?.setPlaybackRate(speed.value);
248
+ }
249
+ };
250
+
251
+ const loadSleep = async () => {
252
+ const sleep = await loadItemsFromStorage("sleep");
253
+
254
+ if (playerState.isReadyToPlay && sleep) {
255
+ const { futureSleepDate, ...rest } = sleep;
256
+ setSleepTimer(rest);
257
+ const sleepTimerDate = await handleSleep(sleep.value, futureSleepDate);
258
+ setSleepTimerDate(sleepTimerDate);
259
+ }
260
+ };
261
+
262
+ loadSpeed().then();
263
+ loadSleep().then();
264
+ }, [playerState.isReadyToPlay]);
265
+
134
266
  const getValue = (key: string, stylesObj: {}) =>
135
267
  R.propOr(null, key, stylesObj);
136
268
 
@@ -170,6 +302,60 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
170
302
  player?.forward(seekDuration);
171
303
  }, [seekDuration, player]);
172
304
 
305
+ const onSpeedSelect = React.useCallback(
306
+ async (item) => {
307
+ setPlaybackSpeed(item);
308
+ player.setPlaybackRate(item.value);
309
+ await localStorage.setItem("speed", item, NAMESPACE);
310
+ },
311
+ [player, setPlaybackSpeed]
312
+ );
313
+
314
+ const onSleepSelect = React.useCallback(
315
+ async (item) => {
316
+ setSleepTimer(item);
317
+ const futureSleepDate = await handleSleep(item.value);
318
+ setSleepTimerDate(futureSleepDate);
319
+
320
+ await localStorage.setItem(
321
+ "sleep",
322
+ { ...item, futureSleepDate },
323
+ NAMESPACE
324
+ );
325
+ },
326
+ [player, setSleepTimer]
327
+ );
328
+
329
+ const onSpeedButtonPress = React.useCallback(() => {
330
+ navigator.openModalBottomSheet({
331
+ modalBottomSheetContentProps: {
332
+ items: speeds,
333
+ current_selection: playbackSpeed,
334
+ onPress: onSpeedSelect,
335
+ title: configuration["playback_speed_title"],
336
+ },
337
+ options: {
338
+ supportedOrientations: ["portrait"],
339
+ },
340
+ });
341
+ }, [playbackSpeed, setPlaybackSpeed]);
342
+
343
+ const onSleepButtonPress = React.useCallback(() => {
344
+ navigator.openModalBottomSheet({
345
+ modalBottomSheetContentProps: {
346
+ items: [
347
+ { label: configuration["playback_sleep_off_label"], value: 0 },
348
+ ].concat(sleeps),
349
+ current_selection: sleepTimer,
350
+ onPress: onSleepSelect,
351
+ title: configuration["playback_sleep_title"],
352
+ },
353
+ options: {
354
+ supportedOrientations: ["portrait"],
355
+ },
356
+ });
357
+ }, [sleepTimer, setSleepTimer, configuration]);
358
+
173
359
  const onCloseVideoModal = React.useCallback(() => {
174
360
  playerManager.close();
175
361
  }, [playerManager]);
@@ -213,12 +399,22 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
213
399
  onForwardButtonPress={onForwardButtonPress}
214
400
  onPlayerSeek={onPlayerSeek}
215
401
  onSkipStart={onSkipStart}
402
+ onSpeedButtonPress={onSpeedButtonPress}
403
+ onSleepButtonPress={onSleepButtonPress}
404
+ playbackSpeedName={formatSpeed(playbackSpeed.value)}
405
+ playbackSleepName={
406
+ sleepTimer.value === 0 ? "sleep_timer" : "sleep_timer_active"
407
+ }
408
+ sleepTimerDate={sleepTimerDate}
409
+ cancelSleepTimer={cancelSleepTimer}
216
410
  onSkipLive={onSkipLive}
217
411
  onCloseVideoModal={onCloseVideoModal}
218
412
  maximiseVideoModal={maximiseVideoModal}
219
413
  configuration={configuration}
220
414
  dimensions={dimensions}
221
415
  playNextData={playNextData}
416
+ title={title}
417
+ summary={summary}
222
418
  />
223
419
  {children}
224
420
  {!docked && (
@@ -252,12 +448,25 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
252
448
  playNextData,
253
449
  onSkipStart,
254
450
  onSkipLive,
451
+ sleepTimer.value,
452
+ sleepTimerDate,
255
453
  ]);
256
454
 
257
455
  if (configuration.magic_background) {
456
+ const baseUrl = entry?.extensions?.audio_player_background_image ?? "";
457
+
458
+ const queryParamName =
459
+ configuration.audio_player_background_image_query || "width";
460
+
461
+ const queryParamValue = 15;
462
+
463
+ const url = addQueryParam(baseUrl, queryParamName, queryParamValue);
464
+
258
465
  return (
259
466
  <MagicBG
260
- image={entry?.extensions?.audio_player_background_image}
467
+ image={url}
468
+ fallbackUrl={baseUrl}
469
+ colors={entry?.extensions?.backgroundColors}
261
470
  defaultColor="#000000"
262
471
  >
263
472
  {MainContent}
@@ -6,21 +6,25 @@ import {
6
6
  ProgressBar,
7
7
  Controls,
8
8
  TransportInfo,
9
+ CountDown,
9
10
  } from "@applicaster/quick-brick-mobile-transport-controls";
10
11
 
11
12
  import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
12
13
 
13
- import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks/device/useIsTablet";
14
+ import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
14
15
  import {
15
16
  forwardButton,
16
17
  playbackButton,
17
18
  rewindButton,
18
19
  skipToStartButton,
19
20
  skipToLiveButton,
21
+ speedButton,
22
+ sleepButton,
20
23
  } from "./defaultLayoutsConts";
21
24
  import { AudioDescription } from "../AudioDescription";
22
25
  import { PlayerImageBackground } from "@applicaster/zapp-react-native-ui-components/Components/PlayerImageBackground";
23
26
  import { useLanguageData } from "@applicaster/zapp-react-native-utils/reactHooks/player";
27
+ import { useEffect, useMemo, useState } from "react";
24
28
 
25
29
  type Dimensions = {
26
30
  width: number;
@@ -42,15 +46,23 @@ type LayoutProps = {
42
46
  isPaused: boolean;
43
47
  isBuffering: boolean;
44
48
  };
49
+ sleepTimerDate: number;
45
50
  value: GetValue;
51
+ playbackSpeedName: string;
52
+ playbackSleepName: string;
46
53
  configuration: { [key: string]: any };
47
54
  onPlaybackButtonPress: () => void;
48
55
  onRewindButtonPress: () => void;
49
56
  onForwardButtonPress: () => void;
57
+ onSpeedButtonPress: () => void;
58
+ onSleepButtonPress: () => void;
59
+ cancelSleepTimer: () => void;
50
60
  onPlayerSeek: (targetTime: number) => void;
51
61
  onSkipLive: () => void;
52
62
  onSkipStart: () => void;
53
63
  dimensions: Dimensions;
64
+ title: string | number;
65
+ summary: string | number;
54
66
  };
55
67
 
56
68
  const Styles = {
@@ -68,13 +80,13 @@ const Styles = {
68
80
  },
69
81
  controls: (isTablet: boolean, isLiveShowFullControls: boolean) => ({
70
82
  height: isTablet ? 64 : 50,
71
- marginHorizontal: isLiveShowFullControls ? 10 : 24,
72
- justifyContent: isLiveShowFullControls ? "space-between" : "center",
83
+ marginHorizontal: 10,
84
+ justifyContent: isLiveShowFullControls ? "space-between" : "space-evenly",
73
85
  }),
74
86
  backgroundImageContainer: (dimensions: Dimensions, isTablet: boolean) => {
75
87
  return {
76
- width: isTablet ? 463 : dimensions.width,
77
- height: isTablet ? 463 : dimensions.width,
88
+ width: isTablet ? 463 : dimensions.width || 330,
89
+ height: isTablet ? 463 : dimensions.width || 330,
78
90
  };
79
91
  },
80
92
  backgroundImageContainerImageStyle: (isTablet) => ({
@@ -117,8 +129,16 @@ export default function DefaultLayout(props: LayoutProps) {
117
129
  onPlayerSeek,
118
130
  onSkipStart,
119
131
  onSkipLive,
132
+ onSpeedButtonPress,
133
+ onSleepButtonPress,
134
+ playbackSpeedName,
135
+ playbackSleepName,
120
136
  configuration,
121
137
  dimensions,
138
+ cancelSleepTimer,
139
+ sleepTimerDate,
140
+ title,
141
+ summary,
122
142
  } = props;
123
143
 
124
144
  const isTablet = useIsTablet();
@@ -128,11 +148,17 @@ export default function DefaultLayout(props: LayoutProps) {
128
148
 
129
149
  const languageData = useLanguageData("audio-player");
130
150
 
151
+ const showCounter = useMemo(() => sleepTimerDate > 0, [sleepTimerDate]);
152
+
131
153
  const controller = playerManager.getActivePlayer();
132
154
  const liveButton = Boolean(value("live_badge_enabled"));
155
+ const sleepTitle = value("playback_sleep_countdown_label");
156
+
157
+ const [timeLeft, setTimeLeft] = useState(
158
+ sleepTimerDate !== 0 ? sleepTimerDate - Date.now() : 0
159
+ );
133
160
 
134
- let showFullControls =
135
- controller?.hasContentDuration?.() && !controller?.isAd();
161
+ let showFullControls = !controller?.isAd();
136
162
 
137
163
  if (isLiveContent) {
138
164
  const seekEnabled = Boolean(value("liveSeekingEnabled"));
@@ -163,8 +189,27 @@ export default function DefaultLayout(props: LayoutProps) {
163
189
  return [play];
164
190
  }
165
191
 
166
- const rewind = rewindButton({ isTablet, onPress: onRewindButtonPress });
167
- const forward = forwardButton({ isTablet, onPress: onForwardButtonPress });
192
+ const rewind = rewindButton({
193
+ isTablet,
194
+ onPress: controller?.hasContentDuration?.() ? onRewindButtonPress : null,
195
+ });
196
+
197
+ const forward = forwardButton({
198
+ isTablet,
199
+ onPress: controller?.hasContentDuration?.() ? onForwardButtonPress : null,
200
+ });
201
+
202
+ const speed = speedButton({
203
+ isTablet,
204
+ onPress: onSpeedButtonPress,
205
+ name: playbackSpeedName,
206
+ });
207
+
208
+ const sleep = sleepButton({
209
+ isTablet,
210
+ onPress: onSleepButtonPress,
211
+ name: playbackSleepName,
212
+ });
168
213
 
169
214
  if (isLiveContent) {
170
215
  const skipToStart = skipToStartButton({
@@ -180,7 +225,8 @@ export default function DefaultLayout(props: LayoutProps) {
180
225
  return [skipToStart, rewind, play, forward, skipToLive];
181
226
  }
182
227
 
183
- return [rewind, play, forward];
228
+ // Temporary empty button until we add sleep button. It will ensure the buttons will not break in their position
229
+ return [speed, rewind, play, forward, sleep];
184
230
  }, [
185
231
  playerState,
186
232
  liveButton,
@@ -188,12 +234,48 @@ export default function DefaultLayout(props: LayoutProps) {
188
234
  onPlaybackButtonPress,
189
235
  onRewindButtonPress,
190
236
  onForwardButtonPress,
237
+ onSpeedButtonPress,
191
238
  showFullControls,
192
239
  onSkipStart,
193
240
  onSkipLive,
194
241
  isLiveContent,
242
+ playbackSpeedName,
243
+ playbackSleepName,
244
+ controller,
195
245
  ]);
196
246
 
247
+ useEffect(() => {
248
+ if (showCounter && sleepTimerDate !== 0) {
249
+ // Setting the initial timeLeft based on sleepTimerDate
250
+ setTimeLeft(sleepTimerDate - Date.now());
251
+
252
+ // Setting up an interval to update the remaining time every second
253
+ const intervalId = setInterval(() => {
254
+ setTimeLeft(() => {
255
+ const newTimeLeft = sleepTimerDate - Date.now();
256
+
257
+ // If the new remaining time is less than or equal to 0, clear the interval and trigger the cancelSleepTimer function
258
+ if (newTimeLeft <= 0) {
259
+ clearInterval(intervalId);
260
+
261
+ cancelSleepTimer();
262
+
263
+ return 0;
264
+ } else {
265
+ // Otherwise, update the state with the new remaining time
266
+ return newTimeLeft;
267
+ }
268
+ });
269
+ }, 1000);
270
+
271
+ // Clearing the interval when the component unmounts or dependencies change
272
+ return () => clearInterval(intervalId);
273
+ } else {
274
+ // Ensure the timeLeft state is set correctly if showCounter is false
275
+ setTimeLeft(0);
276
+ }
277
+ }, [sleepTimerDate, showCounter, cancelSleepTimer]);
278
+
197
279
  const Separator = () => <View style={Styles.separator} />;
198
280
 
199
281
  const liveSeekingEnabled = value("liveSeekingEnabled");
@@ -222,13 +304,13 @@ export default function DefaultLayout(props: LayoutProps) {
222
304
  >
223
305
  <View style={isTablet ? Styles.tabletLayout : null}>
224
306
  <PlayerImageBackground
225
- configuration={configuration}
226
307
  entry={entry}
227
308
  style={Styles.backgroundImageContainer(dimensions, isTablet)}
228
309
  imageStyle={Styles.backgroundImageContainerImageStyle(isTablet)}
229
310
  />
230
311
  <AudioDescription
231
- content={entry}
312
+ title={title}
313
+ summary={summary}
232
314
  value={value}
233
315
  layoutState={layoutState}
234
316
  />
@@ -245,6 +327,15 @@ export default function DefaultLayout(props: LayoutProps) {
245
327
  disableFullscreen={true}
246
328
  sliderStyle={2}
247
329
  />
330
+
331
+ {showCounter && timeLeft >= 0 && (
332
+ <CountDown
333
+ timeLeft={timeLeft}
334
+ value={value}
335
+ title={sleepTitle}
336
+ />
337
+ )}
338
+
248
339
  <Separator />
249
340
  </View>
250
341
  )}
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import * as React from "react";
2
2
  import {
3
3
  StyleSheet,
4
4
  TouchableWithoutFeedback,
@@ -6,7 +6,7 @@ import {
6
6
  ViewStyle,
7
7
  } from "react-native";
8
8
  import * as R from "ramda";
9
- import { useDimensions } from "@applicaster/zapp-react-native-utils/reactHooks";
9
+ import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
10
10
 
11
11
  import {
12
12
  ContentInfo,
@@ -48,6 +48,8 @@ type LayoutProps = {
48
48
  maximiseVideoModal: () => void;
49
49
  dimensions: Dimensions;
50
50
  playNextData: PlayNextData;
51
+ title: string | number;
52
+ summary: string | number;
51
53
  };
52
54
 
53
55
  const styles = StyleSheet.create({
@@ -196,11 +198,11 @@ export default function DockedLayout(props: LayoutProps) {
196
198
  playerState,
197
199
  configuration,
198
200
  playNextData,
201
+ title,
202
+ summary,
199
203
  } = props;
200
204
 
201
- const {
202
- deviceInfo: { isTablet },
203
- } = useDimensions("screen", { deviceInfo: true });
205
+ const isTablet = useIsTablet();
204
206
 
205
207
  const visible = true;
206
208
  const liveButton = Boolean(value("live_badge_enabled"));
@@ -234,7 +236,11 @@ export default function DockedLayout(props: LayoutProps) {
234
236
  !configuration.magic_background && backgroundColorStyle(value),
235
237
  ]}
236
238
  >
237
- <ContentInfo content={entry} value={value} docked />
239
+ <ContentInfo
240
+ content={{ ...entry, title, summary }}
241
+ value={value}
242
+ docked
243
+ />
238
244
  </View>
239
245
  </View>
240
246
  </TouchableWithoutFeedback>
@@ -53,8 +53,9 @@ type ButtonsProps = {
53
53
  onPress: () => void;
54
54
  isTablet: boolean;
55
55
  style?: any;
56
+ name?: string;
56
57
  };
57
- type ButtonsByNameProps = ButtonsProps & { name: string };
58
+ type ButtonsByNameProps = ButtonsProps & { name?: string };
58
59
 
59
60
  const rewindButtonByName = ({
60
61
  name,
@@ -79,6 +80,28 @@ const skipButtonByName = ({
79
80
  style,
80
81
  });
81
82
 
83
+ const speedButtonByName = ({
84
+ name = "speed_1",
85
+ onPress,
86
+ style,
87
+ }: ButtonsByNameProps): ControlsButton => ({
88
+ size: "small",
89
+ icon: name,
90
+ onPress,
91
+ style,
92
+ });
93
+
94
+ const sleepButtonByName = ({
95
+ name = "sleep_timer",
96
+ onPress,
97
+ style,
98
+ }: ButtonsByNameProps): ControlsButton => ({
99
+ size: "small",
100
+ icon: name,
101
+ onPress,
102
+ style,
103
+ });
104
+
82
105
  export const rewindButton = (props: ButtonsProps): ControlsButton =>
83
106
  rewindButtonByName({ ...props, name: "rewind" });
84
107
 
@@ -90,3 +113,9 @@ export const skipToStartButton = (props: ButtonsProps): ControlsButton =>
90
113
 
91
114
  export const skipToLiveButton = (props: ButtonsProps): ControlsButton =>
92
115
  skipButtonByName({ ...props, name: "skip_live" });
116
+
117
+ export const speedButton = (props: ButtonsProps): ControlsButton =>
118
+ speedButtonByName({ ...props });
119
+
120
+ export const sleepButton = (props: ButtonsProps): ControlsButton =>
121
+ sleepButtonByName({ ...props });
@@ -24,17 +24,25 @@ type LayoutProps = {
24
24
  isBuffering: boolean;
25
25
  };
26
26
  value: GetValue;
27
+ sleepTimerDate: number;
28
+ playbackSpeedName: string;
29
+ playbackSleepName: string;
27
30
  configuration: { [key: string]: any };
28
31
  onPlaybackButtonPress: () => void;
29
32
  onRewindButtonPress: () => void;
30
33
  onForwardButtonPress: () => void;
34
+ onSpeedButtonPress: () => void;
35
+ onSleepButtonPress: () => void;
31
36
  onCloseVideoModal: () => void;
37
+ cancelSleepTimer: () => void;
32
38
  onPlayerSeek: (targetTime: number) => void;
33
39
  onSkipStart: () => void;
34
40
  onSkipLive: () => void;
35
41
  maximiseVideoModal: () => void;
36
42
  dimensions: Dimensions;
37
43
  playNextData: PlayNextData;
44
+ title: string | number;
45
+ summary: string | number;
38
46
  };
39
47
 
40
48
  export default function Layout(
@@ -4,14 +4,17 @@ import * as React from "react";
4
4
  import { StyleSheet, View } from "react-native";
5
5
  import LinearGradient from "../LinearGradient";
6
6
  import { getDominantColor, getPalettes } from "../../../utils/utils";
7
+ import { useEffect } from "react";
7
8
 
8
9
  type MagicBGProps = {
9
10
  children: React.ReactNode;
10
11
  image: string;
12
+ fallbackUrl?: string;
11
13
  defaultColor: string;
14
+ colors: string[]; // Updated to type string[] as it holds hex values
12
15
  };
13
16
 
14
- const colors = {
17
+ const defaultColors = {
15
18
  overlay: "rgba(0, 0, 0, 0.35)",
16
19
  defaultGradientStart: "rgba(0, 0, 0, 0.25)",
17
20
  defaultGradientEnd: "rgba(0, 0, 0, 0)",
@@ -23,7 +26,7 @@ const styles = StyleSheet.create({
23
26
  },
24
27
  fullScreenOverlay: {
25
28
  ...StyleSheet.absoluteFillObject,
26
- backgroundColor: colors.overlay,
29
+ backgroundColor: defaultColors.overlay,
27
30
  },
28
31
  gradientOverlay: {
29
32
  position: "absolute",
@@ -34,47 +37,81 @@ const styles = StyleSheet.create({
34
37
  },
35
38
  });
36
39
 
40
+ const fetchPaletteAndDominantColor = async (imageUrl: string) => {
41
+ const palette = await getPalettes(imageUrl);
42
+
43
+ const colors = palette.map(
44
+ (color: { r: number; g: number; b: number }) =>
45
+ `rgb(${color.r}, ${color.g}, ${color.b})`
46
+ );
47
+
48
+ const dominant = await getDominantColor(imageUrl);
49
+ const dominantColor = `rgb(${dominant.r}, ${dominant.g}, ${dominant.b})`;
50
+
51
+ return { gradientColors: colors.slice(0, 2), dominantColor };
52
+ };
53
+
37
54
  export const MagicBG: React.FC<MagicBGProps> = ({
38
55
  children,
39
56
  image,
57
+ fallbackUrl = "",
58
+ colors = [],
40
59
  defaultColor,
41
60
  }) => {
42
61
  const [dominantColor, setDominantColor] =
43
62
  React.useState<string>(defaultColor);
44
63
 
45
64
  const [gradientColors, setGradientColors] = React.useState<string[]>([
46
- colors.defaultGradientStart,
47
- colors.defaultGradientEnd,
65
+ defaultColors.defaultGradientStart,
66
+ defaultColors.defaultGradientEnd,
48
67
  ]);
49
68
 
50
- React.useEffect(() => {
69
+ useEffect(() => {
51
70
  const fetchColors = async () => {
52
- if (image) {
71
+ // Check if colors are provided and not empty
72
+ if (colors && colors.length > 0) {
73
+ // Use provided colors for gradient and dominant color
74
+ setGradientColors(colors.slice(0, 2)); // First 2 colors for gradient
75
+ setDominantColor(colors[0]); // First color as dominant
76
+ } else if (image) {
77
+ // If no colors, try fetching from image
53
78
  try {
54
- const palette = await getPalettes(image);
55
-
56
- const colors = palette.map(
57
- (color: { r: number; g: number; b: number }) =>
58
- `rgb(${color.r}, ${color.g}, ${color.b})`
59
- );
60
-
61
- setGradientColors(colors.slice(0, 2));
79
+ const { gradientColors, dominantColor } =
80
+ await fetchPaletteAndDominantColor(image);
62
81
 
63
- const dominant = await getDominantColor(image);
64
- setDominantColor(`rgb(${dominant.r}, ${dominant.g}, ${dominant.b})`);
82
+ setGradientColors(gradientColors);
83
+ setDominantColor(dominantColor);
65
84
  } catch (error) {
66
- setGradientColors([
67
- colors.defaultGradientStart,
68
- colors.defaultGradientEnd,
69
- ]);
85
+ // If image fetch fails, try fallback URL
86
+ try {
87
+ const { gradientColors, dominantColor } =
88
+ await fetchPaletteAndDominantColor(fallbackUrl);
70
89
 
71
- setDominantColor(defaultColor);
90
+ setGradientColors(gradientColors);
91
+ setDominantColor(dominantColor);
92
+ } catch {
93
+ // If everything fails, use default color
94
+ setGradientColors([
95
+ defaultColors.defaultGradientStart,
96
+ defaultColors.defaultGradientEnd,
97
+ ]);
98
+
99
+ setDominantColor(defaultColor);
100
+ }
72
101
  }
102
+ } else {
103
+ // If no image or colors, use default color
104
+ setGradientColors([
105
+ defaultColors.defaultGradientStart,
106
+ defaultColors.defaultGradientEnd,
107
+ ]);
108
+
109
+ setDominantColor(defaultColor);
73
110
  }
74
111
  };
75
112
 
76
113
  fetchColors();
77
- }, [image, defaultColor]);
114
+ }, []);
78
115
 
79
116
  return (
80
117
  <View style={[styles.container, { backgroundColor: dominantColor }]}>
@@ -0,0 +1,33 @@
1
+ import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
2
+ import { parseJsonIfNeeded } from "@applicaster/zapp-react-native-utils/functionUtils";
3
+
4
+ export const NAMESPACE = "audio-player";
5
+
6
+ export async function loadItemsFromStorage(key: string) {
7
+ const storedItems = await localStorage.getItem(key, NAMESPACE);
8
+
9
+ return parseJsonIfNeeded(storedItems) || null;
10
+ }
11
+
12
+ export const hasDatePassed = (timestamp: number) => {
13
+ // Get the current date and time
14
+ const currentDate = new Date();
15
+
16
+ // Compare the current date's timestamp with the given timestamp
17
+ return currentDate.getTime() > timestamp;
18
+ };
19
+
20
+ export const formatSpeed = (value: number) => {
21
+ let formattedValue = value.toString();
22
+
23
+ // If the value ends with ".0", remove it
24
+ if (formattedValue.endsWith(".0")) {
25
+ formattedValue = formattedValue.slice(0, -2);
26
+ }
27
+
28
+ // Replace "." with "_"
29
+ formattedValue = formattedValue.replace(".", "_");
30
+
31
+ // Return the formatted string with "speed_" prefix
32
+ return `speed_${formattedValue}`;
33
+ };
@@ -1,3 +1,5 @@
1
+ import queryString from "query-string";
2
+
1
3
  export const toTypeString = (obj) => {
2
4
  switch (typeof obj) {
3
5
  case "object":
@@ -19,3 +21,24 @@ export const stringsOnlyObject = (obj) => {
19
21
 
20
22
  return strObj;
21
23
  };
24
+
25
+ export const getLiveUrl = (url: string) => {
26
+ // Use the replace method with a regular expression to replace all instances of subClass with livePage
27
+ return url.replace(new RegExp("subClass", "g"), "livePage");
28
+ };
29
+
30
+ export const addQueryParam = (
31
+ baseUrl: string,
32
+ paramName: string,
33
+ paramValue: string | number
34
+ ) => {
35
+ const [urlWithoutQuery, query] = baseUrl.split("?");
36
+
37
+ const parsedQuery = queryString.parse(query);
38
+
39
+ parsedQuery[paramName] = paramValue.toString();
40
+
41
+ const updatedQueryString = queryString.stringify(parsedQuery);
42
+
43
+ return `${urlWithoutQuery}?${updatedQueryString}`;
44
+ };
@@ -1,3 +1 @@
1
- export * from "./AssetsHelper";
2
-
3
1
  export * from "./Utils";
@@ -36,6 +36,8 @@ import { AudioPlayerWrapper } from "./AudioLayer/AudioPlayerWrapper";
36
36
  import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
37
37
  import { getAllAccessibilityProps } from "@applicaster/zapp-react-native-utils/configurationUtils";
38
38
  import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
39
+ import { getTargetScreenDataFromEntry } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useTargetScreenData";
40
+ import { isAppleTV } from "@applicaster/zapp-react-native-ui-components/Helpers/Platform";
39
41
 
40
42
  const RNPlayerManager = {
41
43
  QuickBrickPlayerPlugin: isApplePlatform()
@@ -87,7 +89,7 @@ export default class VideoPlayer extends React.Component<
87
89
  // TODO: Can be deleted, on controller logic
88
90
  paused: false,
89
91
  piped: false,
90
- rate: 1,
92
+ rate: 1.0,
91
93
  // TODO: Hack for analytics, must br removed
92
94
  resume: null,
93
95
 
@@ -368,6 +370,10 @@ export default class VideoPlayer extends React.Component<
368
370
  }
369
371
  };
370
372
 
373
+ setPlaybackRate = (rate = 1.0) => {
374
+ this.setState({ rate });
375
+ };
376
+
371
377
  getCurrentTime = () => {
372
378
  return this.state.currentTime;
373
379
  };
@@ -551,6 +557,26 @@ export default class VideoPlayer extends React.Component<
551
557
  this.props.listener?.onPlayerDetached?.(nativeEvent);
552
558
  };
553
559
 
560
+ onVideoFullscreenPlayerDidDismiss = () => {
561
+ this.props.listener?.onVideoFullscreenPlayerDidDismiss?.();
562
+ };
563
+
564
+ onVideoFullscreenPlayerDidPresent = () => {
565
+ this.props.listener?.onVideoFullscreenPlayerDidPresent?.();
566
+ };
567
+
568
+ onVideoFullscreenPlayerWillDismiss = () => {
569
+ this.props.listener?.onVideoFullscreenPlayerWillDismiss?.();
570
+ };
571
+
572
+ onVideoFullscreenPlayerWillPresent = () => {
573
+ this.props.listener?.onVideoFullscreenPlayerWillPresent?.();
574
+ };
575
+
576
+ onPlayerClose = () => {
577
+ this.props.listener?.onPlayerClose?.();
578
+ };
579
+
554
580
  onPiPEnter = () => {
555
581
  this.setState({ piped: true });
556
582
  this.props.openPiP();
@@ -755,7 +781,6 @@ export default class VideoPlayer extends React.Component<
755
781
  render() {
756
782
  const {
757
783
  castAction,
758
- controls,
759
784
  docked,
760
785
  entry,
761
786
  fullscreen,
@@ -767,7 +792,8 @@ export default class VideoPlayer extends React.Component<
767
792
  style,
768
793
  } = this.props;
769
794
 
770
- const { audioTrackId, keyEvent, paused, seek, textTrackId } = this.state;
795
+ const { audioTrackId, keyEvent, paused, seek, textTrackId, rate } =
796
+ this.state;
771
797
 
772
798
  const isAudioItem =
773
799
  entry?.content?.type?.includes?.("audio") ||
@@ -812,17 +838,31 @@ export default class VideoPlayer extends React.Component<
812
838
  const liveCatchUpEnabled =
813
839
  this.props.pluginConfiguration?.liveCatchUpEnabled;
814
840
 
815
- const hideNativeControls = isTV()
816
- ? { controls: !this.getPlayNextData() }
817
- : {};
841
+ // TODO: Get screen data more reliable way useCurrentScreenData
842
+ const screenData = getTargetScreenDataFromEntry(this.props.playableItem);
843
+
844
+ const isInlineTV = screenData?.ui_components?.length > 0 && isTV();
818
845
 
819
- const platformSpecificProps = isApplePlatform()
846
+ let platformSpecificProps: any = isApplePlatform()
820
847
  ? {
821
- ...hideNativeControls,
822
848
  resizeMode: nativeResizeMode,
823
849
  }
824
850
  : {};
825
851
 
852
+ const supportsNativeControls =
853
+ !!this.props.controller?.supportsNativeControls?.();
854
+
855
+ const controls = isAppleTV()
856
+ ? supportsNativeControls && !isInlineTV && !this.getPlayNextData()
857
+ : supportsNativeControls && !this.getPlayNextData();
858
+
859
+ const shouldUseNativeControls = controls && !docked;
860
+
861
+ platformSpecificProps = {
862
+ ...platformSpecificProps,
863
+ controls: shouldUseNativeControls,
864
+ };
865
+
826
866
  const pictureInPictureEnabled =
827
867
  this.props.pluginConfiguration?.pictureInPictureEnabled;
828
868
 
@@ -830,7 +870,6 @@ export default class VideoPlayer extends React.Component<
830
870
  entry, // Must be passed first in list
831
871
  ...platformSpecificProps,
832
872
  audioTrackId,
833
- controls,
834
873
  docked,
835
874
  enableAutoAudioTrackSelection,
836
875
  fullscreen, // TODO: Check fullscreen params not crash
@@ -839,6 +878,7 @@ export default class VideoPlayer extends React.Component<
839
878
  nowPlayingEnabled,
840
879
  paused,
841
880
  seek,
881
+ rate,
842
882
  textTrackId,
843
883
  liveSeekingEnabled,
844
884
  liveCatchUpEnabled,
@@ -884,22 +924,17 @@ export default class VideoPlayer extends React.Component<
884
924
  onVideoEnd: this.onVideoEnd, // complete
885
925
  onVideoError: this.onVideoError, // error
886
926
  onVideoProgress: this.onVideoProgress,
887
- onVideoFullscreenPlayerDidDismiss: this.unHandledEvent(
888
- "onVideoFullscreenPlayerDidPresent"
889
- ),
890
- onVideoFullscreenPlayerDidPresent: this.unHandledEvent(
891
- "onVideoFullscreenPlayerDidPresent"
892
- ),
893
- onVideoFullscreenPlayerWillDismiss: this.unHandledEvent(
894
- "onVideoFullscreenPlayerWillDismiss"
895
- ),
896
- onVideoFullscreenPlayerWillPresent: this.unHandledEvent(
897
- "onVideoFullscreenPlayerWillPresent"
898
- ),
927
+ onVideoFullscreenPlayerDidDismiss: this.onVideoFullscreenPlayerDidDismiss,
928
+ onVideoFullscreenPlayerDidPresent: this.onVideoFullscreenPlayerDidPresent,
929
+ onVideoFullscreenPlayerWillDismiss:
930
+ this.onVideoFullscreenPlayerWillDismiss,
931
+ onVideoFullscreenPlayerWillPresent:
932
+ this.onVideoFullscreenPlayerWillPresent,
899
933
  onVideoLoad: this.onVideoLoad,
900
934
  onVideoLoadStart: this.unHandledEvent("onVideoLoadStart"),
901
935
  onVideoBuffer: this.onVideoBuffer,
902
936
  onPlaybackStalled: this.onPlaybackStalled,
937
+ onPlayerClose: this.onPlayerClose,
903
938
  onPlayerAttached: this.onPlayerAttached,
904
939
  onPlayerDetached: this.onPlayerDetached,
905
940
  };
@@ -973,7 +1008,7 @@ export default class VideoPlayer extends React.Component<
973
1008
  !needToShowPlayNextOverlay &&
974
1009
  this.props.mode !== "PIP";
975
1010
 
976
- if (controls === true) {
1011
+ if (shouldUseNativeControls) {
977
1012
  return null;
978
1013
  }
979
1014
 
@@ -1005,10 +1040,12 @@ export default class VideoPlayer extends React.Component<
1005
1040
  inline,
1006
1041
  docked,
1007
1042
  isModal,
1043
+ pip: this.state.piped,
1008
1044
  layoutState: {
1009
1045
  inline,
1010
1046
  docked,
1011
1047
  isModal,
1048
+ pip: this.state.piped,
1012
1049
  },
1013
1050
  configuration: this.getPluginConfiguration(),
1014
1051
  fullscreen: fullscreen,
@@ -1085,6 +1122,7 @@ export default class VideoPlayer extends React.Component<
1085
1122
  listener={this.props.listener}
1086
1123
  style={getStyle(playerDimensions)}
1087
1124
  pluginConfiguration={this.getPluginConfiguration()}
1125
+ controller={this.props.controller}
1088
1126
  >
1089
1127
  {this.childrenWithPlayNext()}
1090
1128
  </this.props.PlayerComponent>
@@ -1121,6 +1159,7 @@ export default class VideoPlayer extends React.Component<
1121
1159
  listener={this.props.listener}
1122
1160
  style={this.takeParentStyle}
1123
1161
  pluginConfiguration={this.getPluginConfiguration()}
1162
+ controller={this.props.controller}
1124
1163
  >
1125
1164
  {this.childrenWithPlayNext()}
1126
1165
  </this.props.PlayerComponent>