├── .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 | 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 | --------------------------------------------------------------------------------