├── .gitignore
├── README.md
├── app.json
├── app
├── _layout.tsx
├── index.tsx
└── videoPlayer.tsx
├── assets
└── images
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── icon.png
│ ├── partial-react-logo.png
│ ├── react-logo.png
│ ├── react-logo@2x.png
│ ├── react-logo@3x.png
│ └── splash.png
├── babel.config.js
├── components
├── Button.tsx
├── ContainButton.tsx
├── CoverButton.tsx
└── StratchButton.tsx
├── constants
└── index.ts
├── mock
└── VideoData.ts
├── package-lock.json
├── package.json
├── tsconfig.json
├── types
└── home.ts
├── utils
└── index.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
16 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
17 | # The following patterns were generated by expo-cli
18 |
19 | expo-env.d.ts
20 | # @end expo-cli
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your Expo app 👋
2 |
3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
4 |
5 | ## Get started
6 |
7 | 1. Install dependencies
8 |
9 | ```bash
10 | npm install
11 | ```
12 |
13 | 2. Start the app
14 |
15 | ```bash
16 | npx expo start
17 | ```
18 |
19 | In the output, you'll find options to open the app in a
20 |
21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/)
22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
25 |
26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
27 |
28 | ## Get a fresh project
29 |
30 | When you're ready, run:
31 |
32 | ```bash
33 | npm run reset-project
34 | ```
35 |
36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
37 |
38 | ## Learn more
39 |
40 | To learn more about developing your project with Expo, look at the following resources:
41 |
42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
44 |
45 | ## Join the community
46 |
47 | Join our community of developers creating universal apps.
48 |
49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
51 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "rn-video-testing",
4 | "slug": "rn-video-testing",
5 | "version": "1.0.0",
6 | "backgroundColor": "#000000",
7 | "orientation": "portrait",
8 | "icon": "./assets/images/icon.png",
9 | "scheme": "myapp",
10 | "userInterfaceStyle": "automatic",
11 | "splash": {
12 | "image": "./assets/images/splash.png",
13 | "resizeMode": "contain",
14 | "backgroundColor": "#ffffff"
15 | },
16 | "ios": {
17 | "supportsTablet": true
18 | },
19 | "android": {
20 | "adaptiveIcon": {
21 | "foregroundImage": "./assets/images/adaptive-icon.png",
22 | "backgroundColor": "#ffffff"
23 | }
24 | },
25 | "web": {
26 | "bundler": "metro",
27 | "output": "static",
28 | "favicon": "./assets/images/favicon.png"
29 | },
30 | "plugins": [
31 | "expo-router",
32 | "expo-video",
33 | [
34 | "expo-screen-orientation",
35 | {
36 | "initialOrientation": "DEFAULT"
37 | }
38 | ]
39 | ],
40 | "experiments": {
41 | "typedRoutes": true
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DarkTheme,
3 | DefaultTheme,
4 | ThemeProvider,
5 | } from "@react-navigation/native";
6 | import { Stack } from "expo-router";
7 | import { Platform, useColorScheme } from "react-native";
8 | import { GestureHandlerRootView } from "react-native-gesture-handler";
9 | import "react-native-reanimated";
10 |
11 | const PlayerOptions = Platform.select({
12 | android: {
13 | statusBarTranslucent: true,
14 | headerShown: false,
15 | },
16 | ios: {
17 | headerShown: false,
18 | },
19 | });
20 |
21 | export default function RootLayout() {
22 | const colorScheme = useColorScheme();
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/app/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FlatList,
3 | StyleSheet,
4 | Image as RNImage,
5 | View,
6 | useWindowDimensions,
7 | Text,
8 | Pressable,
9 | } from "react-native";
10 | import React, { useState } from "react";
11 | import { MEDIA_DATA } from "@/mock/VideoData";
12 | import { Image } from "expo-image";
13 | import { useTheme } from "@react-navigation/native";
14 | import { useSafeAreaInsets } from "react-native-safe-area-context";
15 | import { useRouter } from "expo-router";
16 | import { MEDIA_TYPE } from "@/types/home";
17 |
18 | const RenderItem = ({ item }: { item: MEDIA_TYPE }) => {
19 | const router = useRouter();
20 | const { width } = useWindowDimensions();
21 | const [height, setHeight] = useState(0);
22 |
23 | const theme = useTheme();
24 |
25 | RNImage.getSize(item.thumb, (imgWidth, imgHeight) => {
26 | let aspectRatio = imgWidth / imgHeight;
27 | const imageHeight = width / aspectRatio;
28 | setHeight(imageHeight);
29 | });
30 |
31 | return (
32 |
34 | router.navigate({
35 | pathname: "/videoPlayer",
36 | params: { uri: item.sources },
37 | })
38 | }
39 | >
40 |
45 |
46 |
50 | {item.description}
51 |
52 |
53 | {item.title}
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | const Main = () => {
61 | const { bottom } = useSafeAreaInsets();
62 | return (
63 |
64 | }
68 | keyExtractor={(_, i) => i.toString()}
69 | />
70 |
71 | );
72 | };
73 |
74 | export default Main;
75 |
76 | const styles = StyleSheet.create({
77 | detailsWrapper: {
78 | padding: 15,
79 | gap: 5,
80 | },
81 | descriptionText: {
82 | fontSize: 14,
83 | fontWeight: "500",
84 | },
85 | title: {
86 | fontSize: 14,
87 | color: "rgba(100,100,100,0.8)",
88 | },
89 | });
90 |
--------------------------------------------------------------------------------
/app/videoPlayer.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ActivityIndicator,
3 | Platform,
4 | StyleSheet,
5 | Text,
6 | TextInput,
7 | View,
8 | useWindowDimensions,
9 | } from "react-native";
10 | import React, { useEffect, useMemo, useRef, useState } from "react";
11 | import * as ScreenOrientation from "expo-screen-orientation";
12 | import { Ionicons, MaterialIcons } from "@expo/vector-icons";
13 | import { AVPlaybackStatusSuccess, ResizeMode, Video } from "expo-av";
14 | import { SafeAreaView } from "react-native-safe-area-context";
15 | import { useLocalSearchParams, useNavigation, useRouter } from "expo-router";
16 | import { calculateTimeDifference, formatDuration } from "@/utils";
17 | import Animated, {
18 | FadeIn,
19 | FadeOut,
20 | runOnJS,
21 | useAnimatedStyle,
22 | useSharedValue,
23 | withTiming,
24 | } from "react-native-reanimated";
25 | import { Slider } from "react-native-awesome-slider";
26 | import Button from "@/components/Button";
27 | import { LinearGradient } from "expo-linear-gradient";
28 | import { Gesture, GestureDetector } from "react-native-gesture-handler";
29 | import { LINEAR_GRADIENT_COLORS, SLIDER_THEME } from "@/constants";
30 | import CoverButton from "@/components/CoverButton";
31 | import ContainButton from "@/components/ContainButton";
32 | import StretchButton from "@/components/StratchButton";
33 |
34 | const VideoPlayer = () => {
35 | const { width } = useWindowDimensions();
36 | const navigation = useNavigation();
37 | const local = useLocalSearchParams();
38 | const router = useRouter();
39 |
40 | const timeoutId = useRef(null);
41 |
42 | const videoRef = useRef(null);
43 |
44 | const [state, setState] = useState({
45 | isPlay: true,
46 | fullscreen: false,
47 | startDuration: 0,
48 | currentDuration: 0,
49 | duration: 0,
50 | isBuffering: true,
51 | resizeMode: ResizeMode.CONTAIN,
52 | isControls: false,
53 | isSliding: false,
54 | });
55 |
56 | const cache = useSharedValue(0);
57 |
58 | const progress = useSharedValue(0);
59 | const min = useSharedValue(0);
60 | const max = useSharedValue(100);
61 | const isScrubbing = useSharedValue(false);
62 |
63 | const currentDuration = useSharedValue(0);
64 |
65 | const seekTo = async (time: number = 0) => {
66 | setState((prev) => ({
67 | ...prev,
68 | currentDuration: time,
69 | }));
70 |
71 | await videoRef.current?.playFromPositionAsync(time);
72 | isScrubbing.value = false;
73 | currentDuration.value = time;
74 | };
75 |
76 | const unsetOrientation = async () => {
77 | await ScreenOrientation.lockAsync(
78 | ScreenOrientation.OrientationLock.PORTRAIT_UP
79 | );
80 | };
81 |
82 | useEffect(() => {
83 | return () => {
84 | unsetOrientation();
85 | };
86 | }, []);
87 |
88 | useEffect(() => {
89 | let timeoutId: NodeJS.Timeout;
90 | if (state.isControls) {
91 | timeoutId = setTimeout(() => {
92 | setState((prev) => ({ ...prev, isControls: false }));
93 | }, 3000);
94 | }
95 | return () => clearTimeout(timeoutId);
96 | }, [state]);
97 |
98 | const onPlaybackStatusUpdate = (data: AVPlaybackStatusSuccess) => {
99 | if (!data?.isLoaded) return;
100 | if (data?.durationMillis && data?.playableDurationMillis) {
101 | if (!isScrubbing.value) {
102 | if (data?.durationMillis > data?.positionMillis) {
103 | setState((prev) => ({
104 | ...prev,
105 | currentDuration: data?.positionMillis,
106 | }));
107 | currentDuration.value = data?.positionMillis;
108 | progress.value = data?.positionMillis;
109 | }
110 | }
111 |
112 | if (
113 | data?.durationMillis > data?.playableDurationMillis &&
114 | data?.positionMillis < data?.playableDurationMillis
115 | ) {
116 | cache.value = data?.playableDurationMillis;
117 | }
118 | setState((prev) => ({ ...prev, isBuffering: data?.isBuffering }));
119 | }
120 | };
121 |
122 | const onLoad = (data: AVPlaybackStatusSuccess) => {
123 | if (data.durationMillis) {
124 | max.value = data.durationMillis;
125 | setState((prev) => ({ ...prev, duration: data.durationMillis || 0 }));
126 | }
127 | };
128 |
129 | const togglePlay = () => {
130 | if (state.isPlay) {
131 | videoRef?.current?.pauseAsync();
132 | setState((prev) => ({ ...prev, isPlay: false }));
133 | } else {
134 | videoRef?.current?.playAsync();
135 | setState((prev) => ({ ...prev, isPlay: true }));
136 | }
137 | };
138 |
139 | const prev = () => {
140 | seekTo(state.currentDuration - 10 * 1000);
141 | };
142 | const next = () => {
143 | seekTo(state.currentDuration + 10 * 1000);
144 | };
145 |
146 | const fullscreen = async () => {
147 | if (state.fullscreen) {
148 | if (Platform.OS === "android") {
149 | navigation.setOptions({
150 | navigationBarHidden: false,
151 | statusBarHidden: false,
152 | });
153 | }
154 | setState((prev) => ({ ...prev, fullscreen: false }));
155 | await ScreenOrientation.lockAsync(
156 | ScreenOrientation.OrientationLock.PORTRAIT_UP
157 | );
158 | } else {
159 | if (Platform.OS === "android") {
160 | navigation.setOptions({
161 | navigationBarHidden: true,
162 | statusBarHidden: true,
163 | });
164 | }
165 | setState((prev) => ({ ...prev, fullscreen: true }));
166 | await ScreenOrientation.lockAsync(
167 | ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
168 | );
169 | }
170 | };
171 |
172 | const PlayButton = useMemo(() => {
173 | return (
174 |
181 | );
182 | }, [state.isPlay]);
183 |
184 | const PreviousButton = useMemo(() => {
185 | return (
186 |
189 | );
190 | }, [state.currentDuration]);
191 |
192 | const NextButton = useMemo(() => {
193 | return (
194 |
197 | );
198 | }, [state.currentDuration]);
199 |
200 | const ResizeButton = useMemo(() => {
201 | switch (state.resizeMode) {
202 | case ResizeMode.STRETCH: {
203 | return (
204 |
206 | setState((prev) => ({ ...prev, resizeMode: ResizeMode.COVER }))
207 | }
208 | />
209 | );
210 | }
211 | case ResizeMode.COVER: {
212 | return (
213 |
215 | setState((prev) => ({
216 | ...prev,
217 | resizeMode: ResizeMode.CONTAIN,
218 | }))
219 | }
220 | />
221 | );
222 | }
223 | default: {
224 | return (
225 |
227 | setState((prev) => ({
228 | ...prev,
229 | resizeMode: ResizeMode.STRETCH,
230 | }))
231 | }
232 | />
233 | );
234 | }
235 | }
236 | }, [state.resizeMode]);
237 |
238 | const FullScreenButton = useMemo(() => {
239 | return (
240 |
243 | );
244 | }, [state.fullscreen]);
245 |
246 | const SliderBar = useMemo(() => {
247 | return (
248 | resetTimeout()}
251 | isScrubbing={isScrubbing}
252 | progress={progress}
253 | minimumValue={min}
254 | maximumValue={max}
255 | bubble={(e) => formatDuration(e)}
256 | cache={cache}
257 | thumbWidth={25}
258 | bubbleTranslateY={-50}
259 | bubbleTextStyle={styles.bubbleTextStyle}
260 | theme={SLIDER_THEME}
261 | containerStyle={styles.sliderContainerStyle}
262 | onSlidingStart={() => videoRef?.current?.pauseAsync()}
263 | onSlidingComplete={(val) => {
264 | seekTo(val);
265 | state.isPlay && videoRef?.current?.playAsync();
266 | }}
267 | onValueChange={(val) => {
268 | resetTimeout();
269 | progress.value = val;
270 | }}
271 | />
272 | );
273 | }, [state.isPlay]);
274 |
275 | const GradientWrapper = useMemo(() => {
276 | return (
277 |
281 | );
282 | }, []);
283 |
284 | const Header = () => {
285 | return (
286 |
287 |
290 |
291 | );
292 | };
293 |
294 | const resetTimeout = () => {
295 | setState((prev) => ({ ...prev, isControls: true }));
296 | if (timeoutId.current) {
297 | clearTimeout(timeoutId.current);
298 | }
299 | timeoutId.current = setTimeout(() => {
300 | setState((prev) => ({ ...prev, isControls: false }));
301 | }, 3000);
302 | };
303 |
304 | const TapHandler = Gesture.Tap().onStart(() => {
305 | runOnJS(resetTimeout)();
306 | });
307 |
308 | const Controls = useMemo(() => {
309 | return (
310 |
311 |
312 |
313 | {/* SlideBar */}
314 |
315 | {SliderBar}
316 | {/* duration */}
317 |
318 |
319 | {formatDuration(state.currentDuration)}
320 |
321 |
322 | {formatDuration(state.duration)}
323 |
324 |
325 | {/* duration */}
326 |
327 | {/* SlideBar */}
328 |
329 | {/* controls */}
330 |
331 | {ResizeButton}
332 |
333 | <>
334 | {PreviousButton}
335 | {PlayButton}
336 | {NextButton}
337 | >
338 |
339 | {FullScreenButton}
340 |
341 | {/* controls */}
342 |
343 |
344 |
345 | );
346 | }, [
347 | state.isControls,
348 | state.currentDuration,
349 | state.duration,
350 | state.resizeMode,
351 | state.isPlay,
352 | state.fullscreen,
353 | ]);
354 |
355 | const Loader = useMemo(() => {
356 | if (!state.isBuffering) {
357 | return null;
358 | }
359 | return (
360 |
366 |
367 |
368 | );
369 | }, [state.isBuffering]);
370 |
371 | const animatedStyle = useAnimatedStyle(() => {
372 | return {
373 | opacity: state.isControls ? withTiming(1) : withTiming(0),
374 | };
375 | });
376 |
377 | const toggleSliding = (val: boolean) => {
378 | val !== state.isSliding &&
379 | setState((prev) => ({ ...prev, isSliding: val }));
380 | };
381 |
382 | const pauseAsync = async () => {
383 | await videoRef?.current?.pauseAsync();
384 | };
385 |
386 | const setStartDuration = (value?: boolean) => {
387 | setState((prev) => ({
388 | ...prev,
389 | startDuration: value
390 | ? state.currentDuration === 0
391 | ? 0
392 | : state.currentDuration
393 | : 0,
394 | }));
395 | };
396 |
397 | const setCurrentDuration = (value: number) => {
398 | setState((prev) => ({
399 | ...prev,
400 | currentDuration: value < 0 ? 0 : value,
401 | }));
402 | };
403 |
404 | const SlideHandler = Gesture.Pan()
405 | .onStart((e) => {
406 | runOnJS(pauseAsync)();
407 | runOnJS(setStartDuration)(true);
408 | })
409 | .onChange((e) => {
410 | runOnJS(toggleSliding)(true);
411 | let progressValue =
412 | currentDuration.value + (max.value / width / 10) * e.translationX;
413 | progress.value = progressValue;
414 | runOnJS(setCurrentDuration)(+progressValue.toFixed(0));
415 | })
416 | .onFinalize(() => {
417 | currentDuration.value = progress.value;
418 | runOnJS(setStartDuration)(false);
419 | runOnJS(toggleSliding)(false);
420 | runOnJS(seekTo)(progress.value);
421 | });
422 |
423 | const VideoController = useMemo(
424 | () => (
425 |
426 |
430 | {state.isSliding && (
431 |
432 |
433 | {formatDuration(state.currentDuration)}
434 |
435 |
436 | {`(${calculateTimeDifference(
437 | state.startDuration,
438 | state.currentDuration
439 | )})`}
440 |
441 |
442 | )}
443 |
447 | {GradientWrapper}
448 |
449 |
450 |
451 |
452 |
455 | setState((prev) => ({ ...prev, isControls: false }))
456 | }
457 | />
458 | {Controls}
459 |
460 |
461 |
462 |
463 | ),
464 | [
465 | state.isSliding,
466 | state.isControls,
467 | state.startDuration,
468 | state.currentDuration,
469 | state.duration,
470 | state.resizeMode,
471 | state.isPlay,
472 | state.fullscreen,
473 | ]
474 | );
475 |
476 | return (
477 |
478 |
490 | {Loader}
491 | {VideoController}
492 |
493 | );
494 | };
495 |
496 | export default VideoPlayer;
497 |
498 | const styles = StyleSheet.create({
499 | container: {
500 | flex: 1,
501 | backgroundColor: "black",
502 | },
503 | durationTextStyle: {
504 | color: "white",
505 | fontSize: 13,
506 | },
507 | controllerContainer: {
508 | flex: 1,
509 | paddingHorizontal: 20,
510 | paddingTop: 10,
511 | },
512 | loaderContainer: {
513 | ...StyleSheet.absoluteFillObject,
514 | justifyContent: "center",
515 | alignItems: "center",
516 | backgroundColor: "rgba(0,0,0,0.3)",
517 | },
518 | controllerWrapper: {
519 | position: "absolute",
520 | bottom: 0,
521 | width: "100%",
522 | },
523 | controllerSubWrapper: {
524 | flex: 1,
525 | gap: 0,
526 | },
527 | durationWrapper: {
528 | flexDirection: "row",
529 | justifyContent: "space-between",
530 | },
531 | resizeButtonContainer: {
532 | flexDirection: "row",
533 | justifyContent: "space-between",
534 | alignItems: "center",
535 | paddingHorizontal: 20,
536 | paddingVertical: 10,
537 | },
538 | playButtonContainer: {
539 | flexDirection: "row",
540 | justifyContent: "center",
541 | alignItems: "center",
542 | gap: 50,
543 | },
544 | headerContainer: {
545 | height: 200,
546 | position: "absolute",
547 | top: 0,
548 | width: "100%",
549 | },
550 | headerButtonStyle: {
551 | width: 35,
552 | aspectRatio: 1,
553 | borderRadius: 100,
554 | overflow: "hidden",
555 | justifyContent: "center",
556 | alignItems: "center",
557 | },
558 | sliderContainerStyle: {
559 | borderRadius: 100,
560 | },
561 | containerCenter: {
562 | flex: 1,
563 | justifyContent: "center",
564 | alignItems: "center",
565 | },
566 | centerDurationText: {
567 | color: "white",
568 | fontSize: 50,
569 | fontWeight: "500",
570 | },
571 | subCenterDurationText: {
572 | color: "white",
573 | fontSize: 30,
574 | marginTop: 5,
575 | fontWeight: "500",
576 | },
577 | bubbleTextStyle: {
578 | fontSize: 20,
579 | color: "white",
580 | },
581 | });
582 |
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arunabhverma/rn-video-testing/746d1c6eeca322e1acdf5511606f8b4f47a4bd0d/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arunabhverma/rn-video-testing/746d1c6eeca322e1acdf5511606f8b4f47a4bd0d/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arunabhverma/rn-video-testing/746d1c6eeca322e1acdf5511606f8b4f47a4bd0d/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arunabhverma/rn-video-testing/746d1c6eeca322e1acdf5511606f8b4f47a4bd0d/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arunabhverma/rn-video-testing/746d1c6eeca322e1acdf5511606f8b4f47a4bd0d/assets/images/react-logo.png
--------------------------------------------------------------------------------
/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arunabhverma/rn-video-testing/746d1c6eeca322e1acdf5511606f8b4f47a4bd0d/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arunabhverma/rn-video-testing/746d1c6eeca322e1acdf5511606f8b4f47a4bd0d/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arunabhverma/rn-video-testing/746d1c6eeca322e1acdf5511606f8b4f47a4bd0d/assets/images/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Pressable,
3 | PressableProps,
4 | StyleSheet,
5 | Text,
6 | View,
7 | } from "react-native";
8 | import React, { useState } from "react";
9 | import Animated, {
10 | useAnimatedStyle,
11 | withTiming,
12 | } from "react-native-reanimated";
13 |
14 | const Button = (props: PressableProps) => {
15 | const [isPressed, setIsPressed] = useState(false);
16 |
17 | const animatedStyle = useAnimatedStyle(() => {
18 | return {
19 | transform: [
20 | {
21 | scale: isPressed
22 | ? withTiming(0.5, { duration: 200 })
23 | : withTiming(1, { duration: 200 }),
24 | },
25 | ],
26 | };
27 | });
28 |
29 | return (
30 | setIsPressed(true)}
33 | onTouchEnd={() => setIsPressed(false)}
34 | >
35 |
36 | {props.children}
37 |
38 |
39 | );
40 | };
41 |
42 | export default Button;
43 |
--------------------------------------------------------------------------------
/components/ContainButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { PressableProps } from "react-native";
3 | import { MaterialIcons } from "@expo/vector-icons";
4 | import Button from "./Button";
5 |
6 | const ContainButton = (props: PressableProps) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | export default ContainButton;
15 |
--------------------------------------------------------------------------------
/components/CoverButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { PressableProps } from "react-native";
3 | import { Ionicons } from "@expo/vector-icons";
4 | import Button from "./Button";
5 |
6 | const CoverButton = (props: PressableProps) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | export default CoverButton;
15 |
--------------------------------------------------------------------------------
/components/StratchButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { PressableProps } from "react-native";
3 | import { Ionicons } from "@expo/vector-icons";
4 | import Button from "./Button";
5 |
6 | const StretchButton = (props: PressableProps) => {
7 | return (
8 |
11 | );
12 | };
13 |
14 | export default StretchButton;
15 |
--------------------------------------------------------------------------------
/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const SLIDER_THEME = {
2 | cacheTrackTintColor: "rgba(255, 255, 255, 0.5)",
3 | maximumTrackTintColor: "rgba(255, 255, 255, 0.3)",
4 | minimumTrackTintColor: "white",
5 | bubbleBackgroundColor: "rgba(255, 255, 255, 0.3)",
6 | bubbleTextColor: "white",
7 | };
8 |
9 | export const LINEAR_GRADIENT_COLORS = [
10 | "rgba(0,0,0,0.5)",
11 | "transparent",
12 | "rgba(0,0,0,0.9)",
13 | ];
14 |
--------------------------------------------------------------------------------
/mock/VideoData.ts:
--------------------------------------------------------------------------------
1 | export const MEDIA_DATA = [
2 | {
3 | description:
4 | "Big Buck Bunny tells the story of a giant rabbit with a heart bigger than himself. When one sunny day three rodents rudely harass him, something snaps... and the rabbit ain't no bunny anymore! In the typical cartoon tradition he prepares the nasty rodents a comical revenge.\n\nLicensed under the Creative Commons Attribution license\nhttp://www.bigbuckbunny.org",
5 | sources:
6 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
7 | subtitle: "By Blender Foundation",
8 | // thumb: "images/BigBuckBunny.jpg",
9 | thumb: "https://i.ytimg.com/vi/aqz-KE-bpKQ/maxresdefault.jpg",
10 | title: "Big Buck Bunny",
11 | },
12 | {
13 | description: "The first Blender Open Movie from 2006",
14 | sources:
15 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
16 | subtitle: "By Blender Foundation",
17 | // thumb: "images/ElephantsDream.jpg",
18 | thumb:
19 | "https://upload.wikimedia.org/wikipedia/commons/e/e4/Elephants_Dream_cover.jpg",
20 | title: "Elephant Dream",
21 | },
22 | {
23 | description:
24 | "Sintel is an independently produced short film, initiated by the Blender Foundation as a means to further improve and validate the free/open source 3D creation suite Blender. With initial funding provided by 1000s of donations via the internet community, it has again proven to be a viable development model for both open 3D technology as for independent animation film.\nThis 15 minute film has been realized in the studio of the Amsterdam Blender Institute, by an international team of artists and developers. In addition to that, several crucial technical and creative targets have been realized online, by developers and artists and teams all over the world.\nwww.sintel.org",
25 | sources:
26 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
27 | subtitle: "By Blender Foundation",
28 | // thumb: "images/Sintel.jpg",
29 | thumb: "https://i.ytimg.com/vi/IN6w6GnN-Ic/maxresdefault.jpg",
30 | title: "Sintel",
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rn-video-testing",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "reset-project": "node ./scripts/reset-project.js",
8 | "android": "expo start --android",
9 | "ios": "expo start --ios",
10 | "web": "expo start --web",
11 | "test": "jest --watchAll",
12 | "lint": "expo lint"
13 | },
14 | "jest": {
15 | "preset": "jest-expo"
16 | },
17 | "dependencies": {
18 | "@expo/vector-icons": "^14.0.0",
19 | "@react-navigation/elements": "^1.3.30",
20 | "@react-navigation/native": "^6.0.2",
21 | "expo": "~51.0.14",
22 | "expo-av": "~14.0.5",
23 | "expo-blur": "~13.0.2",
24 | "expo-constants": "~16.0.2",
25 | "expo-font": "~12.0.7",
26 | "expo-image": "~1.12.12",
27 | "expo-linear-gradient": "~13.0.2",
28 | "expo-linking": "~6.3.1",
29 | "expo-router": "~3.5.16",
30 | "expo-screen-orientation": "~7.0.5",
31 | "expo-splash-screen": "~0.27.5",
32 | "expo-status-bar": "~1.12.1",
33 | "expo-system-ui": "~3.0.6",
34 | "expo-video": "^1.1.10",
35 | "expo-web-browser": "~13.0.3",
36 | "m3u-parser-generator": "^1.7.2",
37 | "moment": "^2.30.1",
38 | "react": "18.2.0",
39 | "react-dom": "18.2.0",
40 | "react-native": "0.74.2",
41 | "react-native-awesome-slider": "^2.5.3",
42 | "react-native-gesture-handler": "~2.16.1",
43 | "react-native-reanimated": "~3.10.1",
44 | "react-native-safe-area-context": "4.10.1",
45 | "react-native-screens": "3.31.1",
46 | "react-native-web": "~0.19.10"
47 | },
48 | "devDependencies": {
49 | "@babel/core": "^7.20.0",
50 | "@types/jest": "^29.5.12",
51 | "@types/react": "~18.2.45",
52 | "@types/react-test-renderer": "^18.0.7",
53 | "jest": "^29.2.1",
54 | "jest-expo": "~51.0.1",
55 | "react-test-renderer": "18.2.0",
56 | "typescript": "~5.3.3"
57 | },
58 | "private": true
59 | }
60 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": [
7 | "./*"
8 | ]
9 | }
10 | },
11 | "include": [
12 | "**/*.ts",
13 | "**/*.tsx",
14 | ".expo/types/**/*.ts",
15 | "expo-env.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/types/home.ts:
--------------------------------------------------------------------------------
1 | export interface MEDIA_TYPE {
2 | description: string;
3 | sources: string;
4 | subtitle: string;
5 | thumb: string;
6 | title: string;
7 | }
8 |
9 | export type MEDIA_TYPE_ARRAY = MEDIA_TYPE[];
10 |
--------------------------------------------------------------------------------
/utils/index.ts:
--------------------------------------------------------------------------------
1 | import moment from "moment";
2 |
3 | export const formatDuration = (time: number) => {
4 | return moment.utc(time).format("m:ss");
5 | };
6 |
7 | export const calculateTimeDifference = (
8 | startMillis: number,
9 | endMillis: number
10 | ) => {
11 | const differenceInMillis = endMillis - startMillis;
12 | const isPositive = differenceInMillis >= 0;
13 | const absDifference = Math.abs(differenceInMillis);
14 | const formattedDifference = formatDuration(absDifference);
15 |
16 | return `${isPositive ? "+" : "-"}${formattedDifference}`;
17 | };
18 |
--------------------------------------------------------------------------------