Full Code of ihmpavel/expo-video-player for AI

master d9a71db1e9c7 cached
34 files
73.8 KB
18.8k tokens
16 symbols
1 requests
Download .txt
Repository: ihmpavel/expo-video-player
Branch: master
Commit: d9a71db1e9c7
Files: 34
Total size: 73.8 KB

Directory structure:
gitextract_337zigfa/

├── .eslintrc.js
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── dependabot.yml
├── .gitignore
├── .npmignore
├── .prettierrc.js
├── .vscode/
│   └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist/
│   ├── constants.d.ts
│   ├── constants.js
│   ├── index.d.ts
│   ├── index.js
│   ├── props.d.ts
│   ├── props.js
│   ├── utils.d.ts
│   └── utils.js
├── example-app/
│   ├── .expo-shared/
│   │   └── assets.json
│   ├── .gitignore
│   ├── App.tsx
│   ├── app.json
│   ├── babel.config.js
│   ├── package.json
│   └── tsconfig.json
├── lib/
│   ├── constants.tsx
│   ├── index.tsx
│   ├── props.tsx
│   └── utils.tsx
├── migration-1x-to-2x.md
├── package.json
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.js
================================================
// https://robertcooper.me/post/using-eslint-and-prettier-in-a-typescript-project
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:prettier/recommended',
  ],
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  rules: {
    '@typescript-eslint/semi': ['error', 'never'],
    '@typescript-eslint/no-use-before-define': [
      'error',
      { functions: false, classes: false, variables: false, typedefs: true },
    ],
    '@typescript-eslint/explicit-function-return-type': 0,
    '@typescript-eslint/prefer-interface': 0,
    '@typescript-eslint/interface-name-prefix': 0,
    '@typescript-eslint/no-non-null-assertion': 0,
    '@typescript-eslint/explicit-module-boundary-types': 0,
    '@typescript-eslint/camelcase': 0,
    '@typescript-eslint/ban-ts-ignore': 0,
    '@typescript-eslint/explicit-member-accessibility': 0,
    semi: 'off',
    eqeqeq: 'error',
    'arrow-parens': ['error', 'as-needed'],
    'no-use-before-define': ['error', { functions: false, classes: false, variables: false }],
    'prefer-arrow-callback': 1,
    'no-use-before-define': 0,
    'max-len': ['warn', { code: 100, ignoreComments: true, ignorePattern: '^import .*' }],
    'new-parens': 'error',
    'no-bitwise': 'error',
    'no-console': ['warn', { allow: ['warn', 'info', 'error'] }],
    'no-caller': 'error',
    'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1, maxBOF: 0 }],
    'quote-props': ['error', 'as-needed'],
    'sort-imports-es6-autofix/sort-imports-es6': [
      2,
      {
        ignoreCase: false,
        ignoreMemberSort: false,
        memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
      },
    ],
    'no-irregular-whitespace': 'warn',
    'react/jsx-uses-react': 'off',
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
  },
  plugins: ['sort-imports-es6-autofix', 'react-hooks'],
  settings: {
    react: {
      version: 'detect',
    },
  },
}


================================================
FILE: .github/FUNDING.yml
================================================
github: ihmpavel


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

Link to reproduction on (Snack)[https://snack.expo.io/]

**Expected behavior**
A clear and concise description of what you expected to happen.

**Additional information:**
 - Type: [e.g. Simulator/Expo/Snack/Real device]
 - Device: [e.g. iPhone X]
 - OS: [e.g. iOS8.1]
 - Package version [e.g. 1.0.0]
 - Expo version (in `app.json`)
 - Expo CLI version

**Additional context**
Add any other context about the problem here.

**Screenshots** (if applicable)


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: npm
  directory: "/"
  schedule:
    interval: weekly
    time: "04:00"
  open-pull-requests-limit: 10


================================================
FILE: .gitignore
================================================
node_modules/**/*


================================================
FILE: .npmignore
================================================
lib/
example-app/
.vscode/
.github/
.eslintrc.js
.prettierrc.js
tsconfig.json
jest.config.js


================================================
FILE: .prettierrc.js
================================================
module.exports = {
  semi: false,
  useTabs: false,
  trailingComma: "es5",
  singleQuote: true,
  printWidth: 100,
  tabWidth: 2,
  arrowParens: "avoid",
  jsxSingleQuote: true
};

================================================
FILE: .vscode/settings.json
================================================
{
    "typescript.tsdk": "node_modules\\typescript\\lib",
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {
            "language": "typescript",
            "autoFix": true
        },
        {
            "language": "typescriptreact",
            "autoFix": true
        }
    ],
    "editor.formatOnSave": true,
    "[javascript]": {
        "editor.formatOnSave": false,
    },
    "[javascriptreact]": {
        "editor.formatOnSave": false,
    },
    "[typescript]": {
        "editor.formatOnSave": false,
    },
    "[typescriptreact]": {
        "editor.formatOnSave": false,
    },
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    },
}

================================================
FILE: CHANGELOG.md
================================================
# ChangeLog

## 2.2.0 (September 29, 2022)
- Fix: Prevent accidental pressing of buttons in overlay header when the overlay is not visible by [@lpezzolla](https://github.com/lpezzolla) [#724](https://github.com/ihmpavel/expo-video-player/pull/724)
- Fix: Use fullscreen icons passed in props as an alternative to package-defined icons by [@lpezzolla](https://github.com/lpezzolla) [#727](https://github.com/ihmpavel/expo-video-player/pull/727)

## 2.1.0 (June 24, 2022)
- Enhancements: Updated packages and bumped Expo SDK version
- Enhancements: Added `mute` functionality

## 2.0.4 (January 24, 2021)
- Fix: Replay icon on iOS [#469](https://github.com/ihmpavel/expo-video-player/issues/469)

## 2.0.3 (December 8, 2021)
- Fix: Rebuild app to include `header` on top of the component instead of bottom

## 2.0.2 (December 4, 2021)
- Enhancements: Updated packages and bumped Expo SDK version
- Enhancements: Added `autoHidePlayer` by [@hungvu193](https://github.com/hungvu193) [#506](https://github.com/ihmpavel/expo-video-player/pull/506)
- Enhancements: Added `header` by [@Qeepsake](https://github.com/Qeepsake) [#516](https://github.com/ihmpavel/expo-video-player/pull/516)

## 2.0.1 (June 27, 2021)
- Fix: Expo Web [#433](https://github.com/ihmpavel/expo-video-player/issues/433)

## 2.0.0 (June 20, 2021)
- Rewritten, simplified
- If you are upgrading from version `1.x`, please check [Migration guide to version 2](https://github.com/ihmpavel/expo-video-player/blob/master/migration-1x-to-2x.md)

## 1.6.1 (October 10, 2020)
- Enhancements: Updated packages and bumped Expo SDK version

## 1.6.0 (August 19, 2020)
- Fix: Renamed iosThumbImage to thumbImage
- Enhancements: Remove deprecated Slider in favor of [community version](https://github.com/react-native-community/react-native-slider)
- Enhancements: Added videoRef prop

## 1.5.8 (May 6, 2020)
- Enhancements: Allow disabling Slider

## 1.5.7 (January 31, 2020)
- Fix: Revert removing depracated Slider from RN Core

## 1.5.6 (January 29, 2020)
- Fix: Switch inFullscreen logic
- Enhancements: Remove deprecated Slider

## 1.5.5 (January 1, 2020)
- Happy new Year 🎉
- Fix: Simplify logic

## 1.5.4 (December 29, 2019)
- Fix: Building

## 1.5.3 (December 29, 2019)
- Fix: TypeScript types
- Enhancements: Updated README

## 1.5.2 (December 20, 2019)
- Enhancements: Expo SDK 36
- Enhancements: Updated README

## 1.5.1 (September 30, 2019)
- Fix: Play/Pause icon clicking

## 1.5.0 (August 27, 2019)
- Enhancements: Human readable debug
- Enhancements: Renamed `isPortrait` to `inFullscreen`
- Fix: Removed buggy internet status debug

## 1.4.0 (August 27, 2019)
- Enhancement: Added width/height props to `<Video />` [#7](https://github.com/ihmpavel/expo-video-player/issues/7)
- Enhancement: Added `videoBackground` prop
- Enhancement: Added more video examples
- Enhancement: Checking internet status with hooks

## 1.3.1 (August 25, 2019)
- Fix: Do not add to npm registry Lint source files

## 1.3.0 (August 25, 2019)
- Enhancement: Added changelog
- Enhancement: Fully rewritten, code cleanup
- Enhancement: Rewritten `example app`, now in TS
- Fix: Add setAudioModeAsync key [#2](https://github.com/ihmpavel/expo-video-player/issues/2)
- Fix: Updated Expo [#3](https://github.com/ihmpavel/expo-video-player/issues/3)

## 1.2.0 (April 12, 2019)
- Enhancement: Refactored code

## 1.1.1 (April 12, 2019)
- Enhancement: Updated dependencies

## 1.1.0 (April 12, 2019)
- Fix: Crashing app

## 1.0.0 (December 22, 2018)
- Initial release 🙌🤗

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 @ihmpavel/expo-video-player

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
# Basic info
Video wrappper component for Expo ecosystem built on top of the Expo's [Video component](https://docs.expo.io/versions/latest/sdk/video/). This library basically adds UI controls like in the YouTube app, which gives you the opportunity to play, pause, replay, change video position and a lot of styling options.

The package has a lot of configuration options to fit all your needs. Only `source` in `videoProps: { source: {} }` is required. Check the <a href='#props'>Props</a> table below.

For compatibility information, scroll down to <a href='#compatibility'>Compatibility</a>. The FAQ is <a href='#faq'>here</a>

## ⚠️ Updating from version 1.x to 2.x
If you are updating from version 1.x to 2.x, there are some breaking changes in the API. Please visit [Migration guide to version 2](https://github.com/ihmpavel/expo-video-player/blob/master/migration-1x-to-2x.md) to make your transition as easy as possible. In version 2.x [@react-native-community/netinfo](https://github.com/react-native-netinfo/react-native-netinfo) has been removed.

## Installation
- Install Video Player component typing into terminal `yarn add expo-video-player` _or_ `npm install expo-video-player`
- You also need `expo-av` and `@react-native-community/slider`. Install them with `expo-cli` (`expo install expo-av @react-native-community/slider`)

## Usage
The showcase of some of the possibilities you can create is in the folder [example-app](https://github.com/ihmpavel/expo-video-player/blob/master/example-app). There is Fullscreen, ref, local file, custom icons, styling...

Minimal code to make `VideoPlayer` working
```
import { ResizeMode } from 'expo-av'
import VideoPlayer from 'expo-video-player'

<VideoPlayer
  videoProps={{
    shouldPlay: true,
    resizeMode: ResizeMode.CONTAIN,
    // ❗ source is required https://docs.expo.io/versions/latest/sdk/video/#props
    source: {
      uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
    },
  }}
/>
```

## Props
For default prop values, please visit [/lib/props.tsx](https://github.com/ihmpavel/expo-video-player/blob/master/lib/props.tsx#L11)

| Property | Type | Description |
| ---- | :-------: | ----------- |
| **videoProps** | [`VideoProps`](https://docs.expo.io/versions/latest/sdk/video/#props) | At least `source` is required |
| **errorCallback** | (error: ErrorType) => void | Function which is fired when an error occurs |
| **playbackCallback** | (status: AVPlaybackStatus) => void | Function which is fired every time `onPlaybackStatusUpdate` occurs |
| **defaultControlsVisible** | `boolean` | Show controls on darker overlay when video starts playing. Default is `false` |
| **timeVisible** | `boolean` | Show current time and final length in the bottom. Default is `true` |
| **textStyle** | `TextStyle` | Object containing `<Text />` styling |
| **slider** | `{ visible?: boolean } & SliderProps` | Object containing any of [@react-native-community/slider](https://github.com/callstack/react-native-slider) props. Your styling may break default layout. Also hide slider by providing `visible: false` prop. You are unable to overwrite `ref`, `value`, `onSlidingStart` and `onSlidingComplete` |
| **activityIndicator** | `ActivityIndicatorProps` | Any values from [ActivityIndicator](https://reactnative.dev/docs/activityindicator) |
| **animation** | `{ fadeInDuration?: number, fadeOutDuration?: number }` | Duration of animations in milliseconds |
| **style** | `{ width?: number, height?: number, videoBackgroundColor?: ColorValue, controlsBackgroundColor?: ColorValue }` | Basic styling of `<VideoPlayer />` |
| **icon** | `{ size?: number, color?: ColorValue, style?: TextStyle, pause?: JSX.Element, play?: JSX.Element, replay?: JSX.Element, fullscreen?: JSX.Element, exitFullscreen?: JSX.Element, mute?: JSX.Element, exitMute?: JSX.Element }` | Icon styling. Check more in the [example-app](https://github.com/ihmpavel/expo-video-player/blob/master/example-app/App.tsx) |
| **fullscreen** | `{ enterFullscreen?: () => void, exitFullscreen?: () => void, inFullscreen?: boolean, visible?: boolean }` | Usage of `Fullscreen` mode is in the [example-app](https://github.com/ihmpavel/expo-video-player/blob/master/example-app/App.tsx#L182) |
| **autoHidePlayer** | `boolean` | Prevent player from hiding after certain time, by setting it to `false` you need to tap the screen again to hide the player. Default is `true` |
| **header** | `ReactNode` | Render header component same as in YouTube app. Default `undefined` |
| **mute** | `{ enterMute?: () => void, exitMute?: () => void, isMute?: boolean, visible?: boolean }` | Usage of `mute` mode is in the [example-app](example-app/App.tsx) |

## Compatibility
Library version | Expo SDK version
---- | -------
2.1.x | >= SDK 45
2.x.x | >= SDK 38
1.6.x | >= SDK 38
1.5.x | >= SDK 34
1.4.x | >= SDK 34
1.3.x | >= SDK 34
1.2.x | >= SDK 33
1.1.x | >= SDK 32
1.x.x | >= SDK 32

### CHANGELOG
Changelog added in version 1.3.0
Read [CHANGELOG.md](https://github.com/ihmpavel/expo-video-player/blob/master/CHANGELOG.md)

### FAQ
- **How to make fullscreen working?** Please visit [example-app](https://github.com/ihmpavel/expo-video-player/blob/master/example-app/App.tsx#L182)
- **How to use ref?** Please visit [example-app](https://github.com/ihmpavel/expo-video-player/blob/master/example-app/App.tsx)
- **What to do if I disconnect from the internet while playing video from remote source?** You need to stop/pause playback yourself. I highly recommend using [@react-native-community/netinfo](https://github.com/react-native-netinfo/react-native-netinfo) for this kind of stuff
- **Do you support subtitles?** Have a look at [#1](https://github.com/ihmpavel/expo-video-player/issues/1)
- **Can I support you?** Yes, please [Become a sponsor](https://github.com/sponsors/ihmpavel)

### TODO
- [ ] make tests

#### Some articles
 - Inspired by [expo/videoplayer](https://github.com/expo/videoplayer) _(already deprecated)_
 - [Typescript default props](https://github.com/typescript-cheatsheets/react/issues/415)
 - [Creating a typescript module](https://codeburst.io/https-chidume-nnamdi-com-npm-module-in-typescript-12b3b22f0724)
 - [Creating a component for React](https://medium.com/@BrodaNoel/how-to-create-a-react-component-and-publish-it-in-npm-668ad7d363ce)


## More packages from me
- [all-iso-language-codes](https://github.com/ihmpavel/all-iso-language-codes) - List of ISO 639-1, 639-2T, 639-2B and 639-3 codes with translations in all available languages
- [expo-video-player](https://github.com/ihmpavel/expo-video-player) - Customizable Video Player controls for Expo
- [free-email-domains-list](https://github.com/ihmpavel/free-email-domains-list) - Fresh list of all free email domain providers. Can be used to check if an email address belongs to a company. Updated weekly


================================================
FILE: dist/constants.d.ts
================================================
export declare enum ControlStates {
    Visible = "Visible",
    Hidden = "Hidden"
}
export declare enum PlaybackStates {
    Loading = "Loading",
    Playing = "Playing",
    Paused = "Paused",
    Buffering = "Buffering",
    Error = "Error",
    Ended = "Ended"
}
export declare enum ErrorSeverity {
    Fatal = "Fatal",
    NonFatal = "NonFatal"
}
export declare type ErrorType = {
    type: ErrorSeverity;
    message: string;
    obj: Record<string, unknown>;
};


================================================
FILE: dist/constants.js
================================================
export var ControlStates;
(function (ControlStates) {
    ControlStates["Visible"] = "Visible";
    ControlStates["Hidden"] = "Hidden";
})(ControlStates || (ControlStates = {}));
export var PlaybackStates;
(function (PlaybackStates) {
    PlaybackStates["Loading"] = "Loading";
    PlaybackStates["Playing"] = "Playing";
    PlaybackStates["Paused"] = "Paused";
    PlaybackStates["Buffering"] = "Buffering";
    PlaybackStates["Error"] = "Error";
    PlaybackStates["Ended"] = "Ended";
})(PlaybackStates || (PlaybackStates = {}));
export var ErrorSeverity;
(function (ErrorSeverity) {
    ErrorSeverity["Fatal"] = "Fatal";
    ErrorSeverity["NonFatal"] = "NonFatal";
})(ErrorSeverity || (ErrorSeverity = {}));


================================================
FILE: dist/index.d.ts
================================================
import { AVPlaybackStatus } from 'expo-av';
import { Props } from './props';
import React from 'react';
declare const VideoPlayer: {
    (tempProps: Props): JSX.Element;
    defaultProps: {
        errorCallback: (error: import("./constants").ErrorType) => void;
        playbackCallback: (status: AVPlaybackStatus) => void;
        defaultControlsVisible: boolean;
        timeVisible: boolean;
        textStyle: import("react-native").TextStyle;
        slider: {
            visible?: boolean | undefined;
        } & import("@react-native-community/slider").SliderProps;
        activityIndicator: import("react-native").ActivityIndicatorProps;
        animation: {
            fadeInDuration?: number | undefined;
            fadeOutDuration?: number | undefined;
        };
        header: React.ReactNode;
        style: {
            width?: number | undefined;
            height?: number | undefined;
            videoBackgroundColor?: import("react-native").ColorValue | undefined;
            controlsBackgroundColor?: import("react-native").ColorValue | undefined;
        };
        icon: {
            size?: number | undefined;
            color?: import("react-native").ColorValue | undefined;
            style?: import("react-native").TextStyle | undefined;
            pause?: JSX.Element | undefined;
            play?: JSX.Element | undefined;
            replay?: JSX.Element | undefined;
            loading?: JSX.Element | undefined;
            fullscreen?: JSX.Element | undefined;
            exitFullscreen?: JSX.Element | undefined;
            mute?: JSX.Element | undefined;
            exitMute?: JSX.Element | undefined;
        };
        fullscreen: {
            enterFullscreen?: (() => void) | undefined;
            exitFullscreen?: (() => void) | undefined;
            inFullscreen?: boolean | undefined;
            visible?: boolean | undefined;
        };
        autoHidePlayer: boolean;
        mute: {
            enterMute?: (() => void) | undefined;
            exitMute?: (() => void) | undefined;
            isMute?: boolean | undefined;
            visible?: boolean | undefined;
        };
    };
};
export default VideoPlayer;


================================================
FILE: dist/index.js
================================================
import { __awaiter, __rest } from "tslib";
import { Audio, Video } from 'expo-av';
import { ActivityIndicator, Animated, StyleSheet, Text, TouchableWithoutFeedback, View, } from 'react-native';
import { ControlStates, ErrorSeverity, PlaybackStates } from './constants';
import { ErrorMessage, TouchableButton, deepMerge, getMinutesSecondsFromMilliseconds, styles, } from './utils';
import { MaterialIcons } from '@expo/vector-icons';
import { defaultProps } from './props';
import { useEffect, useRef, useState } from 'react';
import React from 'react';
import Slider from '@react-native-community/slider';
const VideoPlayer = (tempProps) => {
    const props = deepMerge(defaultProps, tempProps);
    let playbackInstance = null;
    let controlsTimer = null;
    let initialShow = props.defaultControlsVisible;
    const header = props.header;
    const [errorMessage, setErrorMessage] = useState('');
    const controlsOpacity = useRef(new Animated.Value(props.defaultControlsVisible ? 1 : 0)).current;
    const [controlsState, setControlsState] = useState(props.defaultControlsVisible ? ControlStates.Visible : ControlStates.Hidden);
    const [playbackInstanceInfo, setPlaybackInstanceInfo] = useState({
        position: 0,
        duration: 0,
        state: props.videoProps.source ? PlaybackStates.Loading : PlaybackStates.Error,
    });
    // We need to extract ref, because of misstypes in <Slider />
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const _a = props.slider, { ref: sliderRef } = _a, sliderProps = __rest(_a, ["ref"]);
    const screenRatio = props.style.width / props.style.height;
    let videoHeight = props.style.height;
    let videoWidth = videoHeight * screenRatio;
    if (videoWidth > props.style.width) {
        videoWidth = props.style.width;
        videoHeight = videoWidth / screenRatio;
    }
    useEffect(() => {
        setAudio();
        return () => {
            if (playbackInstance) {
                playbackInstance.setStatusAsync({
                    shouldPlay: false,
                });
            }
        };
    }, []);
    useEffect(() => {
        if (!props.videoProps.source) {
            console.error('[VideoPlayer] `Source` is a required in `videoProps`. ' +
                'Check https://docs.expo.io/versions/latest/sdk/video/#usage');
            setErrorMessage('`Source` is a required in `videoProps`');
            setPlaybackInstanceInfo(Object.assign(Object.assign({}, playbackInstanceInfo), { state: PlaybackStates.Error }));
        }
        else {
            setPlaybackInstanceInfo(Object.assign(Object.assign({}, playbackInstanceInfo), { state: PlaybackStates.Playing }));
        }
    }, [props.videoProps.source]);
    const hideAnimation = () => {
        Animated.timing(controlsOpacity, {
            toValue: 0,
            duration: props.animation.fadeOutDuration,
            useNativeDriver: true,
        }).start(({ finished }) => {
            if (finished) {
                setControlsState(ControlStates.Hidden);
            }
        });
    };
    const animationToggle = () => {
        if (controlsState === ControlStates.Hidden) {
            Animated.timing(controlsOpacity, {
                toValue: 1,
                duration: props.animation.fadeInDuration,
                useNativeDriver: true,
            }).start(({ finished }) => {
                if (finished) {
                    setControlsState(ControlStates.Visible);
                }
            });
        }
        else if (controlsState === ControlStates.Visible) {
            hideAnimation();
        }
        if (controlsTimer === null && props.autoHidePlayer) {
            controlsTimer = setTimeout(() => {
                if (playbackInstanceInfo.state === PlaybackStates.Playing &&
                    controlsState === ControlStates.Hidden) {
                    hideAnimation();
                }
                if (controlsTimer) {
                    clearTimeout(controlsTimer);
                }
                controlsTimer = null;
            }, 2000);
        }
    };
    // Set audio mode to play even in silent mode (like the YouTube app)
    const setAudio = () => __awaiter(void 0, void 0, void 0, function* () {
        try {
            yield Audio.setAudioModeAsync({
                playsInSilentModeIOS: true,
            });
        }
        catch (e) {
            props.errorCallback({
                type: ErrorSeverity.NonFatal,
                message: 'Audio.setAudioModeAsync',
                obj: e,
            });
        }
    });
    const updatePlaybackCallback = (status) => {
        props.playbackCallback(status);
        if (status.isLoaded) {
            setPlaybackInstanceInfo(Object.assign(Object.assign({}, playbackInstanceInfo), { position: status.positionMillis, duration: status.durationMillis || 0, state: status.positionMillis === status.durationMillis
                    ? PlaybackStates.Ended
                    : status.isBuffering
                        ? PlaybackStates.Buffering
                        : status.shouldPlay
                            ? PlaybackStates.Playing
                            : PlaybackStates.Paused }));
            if ((status.didJustFinish && controlsState === ControlStates.Hidden) ||
                (status.isBuffering && controlsState === ControlStates.Hidden && initialShow)) {
                animationToggle();
                initialShow = false;
            }
        }
        else {
            if (status.isLoaded === false && status.error) {
                const errorMsg = `Encountered a fatal error during playback: ${status.error}`;
                setErrorMessage(errorMsg);
                props.errorCallback({ type: ErrorSeverity.Fatal, message: errorMsg, obj: {} });
            }
        }
    };
    const togglePlay = () => __awaiter(void 0, void 0, void 0, function* () {
        if (controlsState === ControlStates.Hidden) {
            return;
        }
        const shouldPlay = playbackInstanceInfo.state !== PlaybackStates.Playing;
        if (playbackInstance !== null) {
            yield playbackInstance.setStatusAsync(Object.assign({ shouldPlay }, (playbackInstanceInfo.state === PlaybackStates.Ended && { positionMillis: 0 })));
            setPlaybackInstanceInfo(Object.assign(Object.assign({}, playbackInstanceInfo), { state: playbackInstanceInfo.state === PlaybackStates.Playing
                    ? PlaybackStates.Paused
                    : PlaybackStates.Playing }));
            if (shouldPlay) {
                animationToggle();
            }
        }
    });
    if (playbackInstanceInfo.state === PlaybackStates.Error) {
        return (<View style={{
                backgroundColor: props.style.videoBackgroundColor,
                width: videoWidth,
                height: videoHeight,
            }}>
        <ErrorMessage style={props.textStyle} message={errorMessage}/>
      </View>);
    }
    if (playbackInstanceInfo.state === PlaybackStates.Loading) {
        return (<View style={{
                backgroundColor: props.style.controlsBackgroundColor,
                width: videoWidth,
                height: videoHeight,
                justifyContent: 'center',
            }}>
        {props.icon.loading || <ActivityIndicator {...props.activityIndicator}/>}
      </View>);
    }
    return (<View style={{
            backgroundColor: props.style.videoBackgroundColor,
            width: videoWidth,
            height: videoHeight,
            maxWidth: '100%',
        }}>
      <Video style={styles.videoWrapper} {...props.videoProps} ref={component => {
            playbackInstance = component;
            if (props.videoProps.ref) {
                props.videoProps.ref.current = component;
            }
        }} onPlaybackStatusUpdate={updatePlaybackCallback}/>

      <Animated.View pointerEvents={controlsState === ControlStates.Visible ? 'auto' : 'none'} style={[
            styles.topInfoWrapper,
            {
                opacity: controlsOpacity,
            },
        ]}>
        {header}
      </Animated.View>

      <TouchableWithoutFeedback onPress={animationToggle}>
        <Animated.View style={Object.assign(Object.assign({}, StyleSheet.absoluteFillObject), { opacity: controlsOpacity, justifyContent: 'center', alignItems: 'center' })}>
          <View style={Object.assign(Object.assign({}, StyleSheet.absoluteFillObject), { backgroundColor: props.style.controlsBackgroundColor, opacity: 0.5 })}/>
          <View pointerEvents={controlsState === ControlStates.Visible ? 'auto' : 'none'}>
            <View style={styles.iconWrapper}>
              <TouchableButton onPress={togglePlay}>
                <View>
                  {playbackInstanceInfo.state === PlaybackStates.Buffering &&
            (props.icon.loading || <ActivityIndicator {...props.activityIndicator}/>)}
                  {playbackInstanceInfo.state === PlaybackStates.Playing && props.icon.pause}
                  {playbackInstanceInfo.state === PlaybackStates.Paused && props.icon.play}
                  {playbackInstanceInfo.state === PlaybackStates.Ended && props.icon.replay}
                  {((playbackInstanceInfo.state === PlaybackStates.Ended && !props.icon.replay) ||
            (playbackInstanceInfo.state === PlaybackStates.Playing && !props.icon.pause) ||
            (playbackInstanceInfo.state === PlaybackStates.Paused &&
                !props.icon.pause)) && (<MaterialIcons name={playbackInstanceInfo.state === PlaybackStates.Playing
                ? 'pause'
                : playbackInstanceInfo.state === PlaybackStates.Paused
                    ? 'play-arrow'
                    : 'replay'} style={props.icon.style} size={props.icon.size} color={props.icon.color}/>)}
                </View>
              </TouchableButton>
            </View>
          </View>
        </Animated.View>
      </TouchableWithoutFeedback>

      <Animated.View pointerEvents={controlsState === ControlStates.Visible ? 'auto' : 'none'} style={[
            styles.bottomInfoWrapper,
            {
                opacity: controlsOpacity,
            },
        ]}>
        {props.timeVisible && (<Text style={[props.textStyle, styles.timeLeft]}>
            {getMinutesSecondsFromMilliseconds(playbackInstanceInfo.position)}
          </Text>)}
        {props.slider.visible && (<Slider {...sliderProps} style={[styles.slider, props.slider.style]} value={playbackInstanceInfo.duration
                ? playbackInstanceInfo.position / playbackInstanceInfo.duration
                : 0} onSlidingStart={() => {
                if (playbackInstanceInfo.state === PlaybackStates.Playing) {
                    togglePlay();
                    setPlaybackInstanceInfo(Object.assign(Object.assign({}, playbackInstanceInfo), { state: PlaybackStates.Paused }));
                }
            }} onSlidingComplete={(e) => __awaiter(void 0, void 0, void 0, function* () {
                const position = e * playbackInstanceInfo.duration;
                if (playbackInstance) {
                    yield playbackInstance.setStatusAsync({
                        positionMillis: position,
                        shouldPlay: true,
                    });
                }
                setPlaybackInstanceInfo(Object.assign(Object.assign({}, playbackInstanceInfo), { position }));
            })}/>)}
        {props.timeVisible && (<Text style={[props.textStyle, styles.timeRight]}>
            {getMinutesSecondsFromMilliseconds(playbackInstanceInfo.duration)}
          </Text>)}
        {props.mute.visible && (<TouchableButton onPress={() => { var _a, _b, _c, _d; return (props.mute.isMute ? (_b = (_a = props.mute).exitMute) === null || _b === void 0 ? void 0 : _b.call(_a) : (_d = (_c = props.mute).enterMute) === null || _d === void 0 ? void 0 : _d.call(_c)); }}>
            <View>
              {props.icon.mute}
              {props.icon.exitMute}
              {((!props.icon.mute && props.mute.isMute) ||
                (!props.icon.exitMute && !props.mute.isMute)) && (<MaterialIcons name={props.mute.isMute ? 'volume-up' : 'volume-off'} style={props.icon.style} size={props.icon.size / 2} color={props.icon.color}/>)}
            </View>
          </TouchableButton>)}
        {props.fullscreen.visible && (<TouchableButton onPress={() => props.fullscreen.inFullscreen
                ? props.fullscreen.exitFullscreen()
                : props.fullscreen.enterFullscreen()}>
            <View>
              {!props.fullscreen.inFullscreen && props.icon.fullscreen}
              {props.fullscreen.inFullscreen && props.icon.exitFullscreen}
              {((!props.icon.fullscreen && !props.fullscreen.inFullscreen) ||
                (!props.icon.exitFullscreen && props.fullscreen.inFullscreen)) && (<MaterialIcons name={props.fullscreen.inFullscreen ? 'fullscreen-exit' : 'fullscreen'} style={props.icon.style} size={props.icon.size / 2} color={props.icon.color}/>)}
            </View>
          </TouchableButton>)}
      </Animated.View>
    </View>);
};
VideoPlayer.defaultProps = defaultProps;
export default VideoPlayer;


================================================
FILE: dist/props.d.ts
================================================
import { AVPlaybackStatus, Video, VideoProps } from 'expo-av';
import { ActivityIndicatorProps, TextStyle } from 'react-native';
import { ColorValue } from 'react-native';
import { ErrorType } from './constants';
import { MutableRefObject, ReactNode } from 'react';
import { SliderProps } from '@react-native-community/slider';
export declare type Props = RequiredProps & DefaultProps;
export declare const defaultProps: DefaultProps;
declare type RequiredProps = {
    videoProps: VideoProps & {
        ref?: MutableRefObject<Video>;
    };
};
declare type DefaultProps = {
    errorCallback: (error: ErrorType) => void;
    playbackCallback: (status: AVPlaybackStatus) => void;
    defaultControlsVisible: boolean;
    timeVisible: boolean;
    textStyle: TextStyle;
    slider: {
        visible?: boolean;
    } & SliderProps;
    activityIndicator: ActivityIndicatorProps;
    animation: {
        fadeInDuration?: number;
        fadeOutDuration?: number;
    };
    header: ReactNode;
    style: {
        width?: number;
        height?: number;
        videoBackgroundColor?: ColorValue;
        controlsBackgroundColor?: ColorValue;
    };
    icon: {
        size?: number;
        color?: ColorValue;
        style?: TextStyle;
        pause?: JSX.Element;
        play?: JSX.Element;
        replay?: JSX.Element;
        loading?: JSX.Element;
        fullscreen?: JSX.Element;
        exitFullscreen?: JSX.Element;
        mute?: JSX.Element;
        exitMute?: JSX.Element;
    };
    fullscreen: {
        enterFullscreen?: () => void;
        exitFullscreen?: () => void;
        inFullscreen?: boolean;
        visible?: boolean;
    };
    autoHidePlayer: boolean;
    mute: {
        enterMute?: () => void;
        exitMute?: () => void;
        isMute?: boolean;
        visible?: boolean;
    };
};
export {};


================================================
FILE: dist/props.js
================================================
import { Dimensions, Platform } from 'react-native';
export const defaultProps = {
    errorCallback: error => console.error(`[VideoPlayer] ${error.type} Error - ${error.message}: ${error.obj}`),
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    playbackCallback: () => { },
    defaultControlsVisible: false,
    timeVisible: true,
    slider: {
        visible: true,
    },
    textStyle: {
        color: '#FFF',
        fontSize: 12,
        textAlign: 'center',
    },
    activityIndicator: {
        size: 'large',
        color: '#999',
    },
    animation: {
        fadeInDuration: 300,
        fadeOutDuration: 300,
    },
    style: {
        width: Platform.OS === 'web' ? '100%' : Dimensions.get('window').width,
        height: Dimensions.get('window').height,
        videoBackgroundColor: '#000',
        controlsBackgroundColor: '#000',
    },
    icon: {
        size: 48,
        color: '#FFF',
        style: {
            padding: 2,
        },
    },
    fullscreen: {
        enterFullscreen: () => 
        // eslint-disable-next-line no-console
        console.log('[VideoPlayer] - missing `enterFullscreen` function in `fullscreen` prop'),
        exitFullscreen: () => 
        // eslint-disable-next-line no-console
        console.log('[VideoPlayer] - missing `exitFullscreen` function in `fullscreen` prop'),
        inFullscreen: false,
        visible: true,
    },
    autoHidePlayer: true,
    header: undefined,
    mute: {
        enterMute: () => 
        // eslint-disable-next-line no-console
        console.log('[VideoPlayer] - missing `enterMute` function in `mute` prop'),
        exitMute: () => 
        // eslint-disable-next-line no-console
        console.log('[VideoPlayer] - missing `exitMute` function in `mute` prop'),
        isMute: false,
        visible: false,
    },
};


================================================
FILE: dist/utils.d.ts
================================================
import { TextStyle, TouchableNativeFeedbackProps, TouchableOpacityProps } from 'react-native';
import React from 'react';
export declare const ErrorMessage: ({ message, style }: {
    message: string;
    style: TextStyle;
}) => JSX.Element;
export declare const getMinutesSecondsFromMilliseconds: (ms: number) => string;
declare type ButtonProps = (TouchableNativeFeedbackProps | TouchableOpacityProps) & {
    children: React.ReactNode;
};
export declare const TouchableButton: (props: ButtonProps) => JSX.Element;
export declare const deepMerge: (target: {
    [x: string]: any;
}, source: {
    [x: string]: any;
}) => {
    [x: string]: any;
};
export declare const styles: {
    errorWrapper: {
        paddingHorizontal: number;
        justifyContent: "center";
        position: "absolute";
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
    };
    videoWrapper: {
        flex: number;
        justifyContent: "center";
    };
    iconWrapper: {
        borderRadius: number;
        overflow: "hidden";
        padding: number;
    };
    bottomInfoWrapper: {
        position: "absolute";
        flexDirection: "row";
        alignItems: "center";
        justifyContent: "space-between";
        flex: number;
        bottom: number;
        left: number;
        right: number;
    };
    topInfoWrapper: {
        position: "absolute";
        flexDirection: "row";
        alignItems: "center";
        justifyContent: "space-between";
        flex: number;
        top: number;
        left: number;
        right: number;
        zIndex: number;
    };
    timeLeft: {
        backgroundColor: string;
        marginLeft: number;
    };
    timeRight: {
        backgroundColor: string;
        marginRight: number;
    };
    slider: {
        flex: number;
        paddingHorizontal: number;
    };
};
export {};


================================================
FILE: dist/utils.js
================================================
import { Platform, StyleSheet, Text, TouchableNativeFeedback, TouchableOpacity, View, } from 'react-native';
import React from 'react';
export const ErrorMessage = ({ message, style }) => (<View style={styles.errorWrapper}>
    <Text style={style}>{message}</Text>
  </View>);
export const getMinutesSecondsFromMilliseconds = (ms) => {
    const totalSeconds = ms / 1000;
    const seconds = String(Math.floor(totalSeconds % 60));
    const minutes = String(Math.floor(totalSeconds / 60));
    return minutes.padStart(1, '0') + ':' + seconds.padStart(2, '0');
};
export const TouchableButton = (props) => Platform.OS === 'android' ? (<TouchableNativeFeedback background={TouchableNativeFeedback.Ripple('white', true)} {...props}/>) : (<TouchableOpacity {...props}/>);
// https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6#gistcomment-3585151
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const deepMerge = (target, source) => {
    const result = Object.assign(Object.assign({}, target), source);
    const keys = Object.keys(result);
    for (const key of keys) {
        const tprop = target[key];
        const sprop = source[key];
        if (typeof tprop === 'object' && typeof sprop === 'object') {
            result[key] = deepMerge(tprop, sprop);
        }
    }
    return result;
};
export const styles = StyleSheet.create({
    errorWrapper: Object.assign(Object.assign({}, StyleSheet.absoluteFillObject), { paddingHorizontal: 20, justifyContent: 'center' }),
    videoWrapper: {
        flex: 1,
        justifyContent: 'center',
    },
    iconWrapper: {
        borderRadius: 100,
        overflow: 'hidden',
        padding: 10,
    },
    bottomInfoWrapper: {
        position: 'absolute',
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'space-between',
        flex: 1,
        bottom: 0,
        left: 0,
        right: 0,
    },
    topInfoWrapper: {
        position: 'absolute',
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'space-between',
        flex: 1,
        top: 0,
        left: 0,
        right: 0,
        zIndex: 999,
    },
    timeLeft: { backgroundColor: 'transparent', marginLeft: 5 },
    timeRight: { backgroundColor: 'transparent', marginRight: 5 },
    slider: { flex: 1, paddingHorizontal: 10 },
});


================================================
FILE: example-app/.expo-shared/assets.json
================================================
{
  "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true,
  "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true
}

================================================
FILE: example-app/.gitignore
================================================
node_modules/**/*
.expo/*
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
web-report/

# macOS
.DS_Store


================================================
FILE: example-app/App.tsx
================================================
import * as ScreenOrientation from 'expo-screen-orientation'
import { Dimensions, ScrollView, StyleSheet, Text } from 'react-native'
import { ResizeMode } from 'expo-av'
import { setStatusBarHidden } from 'expo-status-bar'
import React, { useRef, useState } from 'react'
import VideoPlayer from 'expo-video-player'

const App = () => {
  const [inFullscreen, setInFullsreen] = useState(false)
  const [inFullscreen2, setInFullsreen2] = useState(false)
  const [isMute, setIsMute] = useState(false)
  const refVideo = useRef(null)
  const refVideo2 = useRef(null)
  const refScrollView = useRef(null)

  return (
    <ScrollView
      scrollEnabled={!inFullscreen2}
      ref={refScrollView}
      onContentSizeChange={() => {
        if (inFullscreen2) {
          refScrollView.current.scrollToEnd({ animated: true })
        }
      }}
      style={styles.container}
      contentContainerStyle={styles.contentContainer}
    >
      <Text style={[styles.text, { fontWeight: 'bold', textTransform: 'uppercase' }]}>
        Examples
      </Text>
      {/* ShouldPlay (autoplay) is true only in the first example */}
      <Text style={styles.text}>Basic</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: true,
          resizeMode: ResizeMode.CONTAIN,
          source: {
            uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
          },
        }}
      />

      <Text style={styles.text}>Local file</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: false,
          resizeMode: ResizeMode.CONTAIN,
          source: require('./local.mp4'),
        }}
        style={{ height: 160 }}
      />

      <Text style={styles.text}>Only video without controls</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: false,
          resizeMode: ResizeMode.CONTAIN,
          source: {
            uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
          },
        }}
        slider={{
          visible: false,
        }}
        fullscreen={{
          visible: false,
        }}
        timeVisible={false}
        style={{ height: 160 }}
      />

      <Text style={styles.text}>Some styling</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: false,
          resizeMode: ResizeMode.CONTAIN,
          source: {
            uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
          },
        }}
        style={{
          videoBackgroundColor: 'transparent',
          controlsBackgroundColor: 'red',
          height: 200,
        }}
      />

      <Text style={styles.text}>With custom icons</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: false,
          resizeMode: ResizeMode.CONTAIN,
          source: {
            uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
          },
        }}
        icon={{
          play: <Text style={{ color: '#FFF' }}>PLAY</Text>,
          pause: <Text style={{ color: '#FFF' }}>PAUSE</Text>,
        }}
        style={{ height: 160 }}
      />

      <Text style={styles.text}>With some more styling</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: false,
          resizeMode: ResizeMode.CONTAIN,
          source: {
            uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
          },
        }}
        style={{
          height: 160,
          width: 160,
          videoBackgroundColor: 'yellow',
          controlsBackgroundColor: 'blue',
        }}
      />

      <Text style={styles.text}>With Mute</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: false,
          resizeMode: ResizeMode.CONTAIN,
          source: {
            uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
          },
          isMuted: isMute,
        }}
        mute={{
          enterMute: () => setIsMute(!isMute),
          exitMute: () => setIsMute(!isMute),
          isMute,
        }}
        style={{ height: 160 }}
      />

      <Text style={styles.text}>Fullscren icon hidden</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: false,
          resizeMode: ResizeMode.CONTAIN,
          source: {
            uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
          },
        }}
        fullscreen={{
          visible: false,
        }}
        style={{ height: 160 }}
      />

      <Text style={styles.text}>Ref - clicking on Enter/Exit fullscreen changes playing</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: false,
          resizeMode: ResizeMode.CONTAIN,
          source: {
            uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
          },
          ref: refVideo,
        }}
        fullscreen={{
          enterFullscreen: () => {
            setInFullsreen(!inFullscreen)
            refVideo.current.setStatusAsync({
              shouldPlay: true,
            })
          },
          exitFullscreen: () => {
            setInFullsreen(!inFullscreen)
            refVideo.current.setStatusAsync({
              shouldPlay: false,
            })
          },
          inFullscreen,
        }}
        style={{ height: 160 }}
      />

      <Text style={styles.text}>Fullscren</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: false,
          resizeMode: ResizeMode.CONTAIN,
          source: {
            uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
          },
          ref: refVideo2,
        }}
        fullscreen={{
          inFullscreen: inFullscreen2,
          enterFullscreen: async () => {
            setStatusBarHidden(true, 'fade')
            setInFullsreen2(!inFullscreen2)
            await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_LEFT)
            refVideo2.current.setStatusAsync({
              shouldPlay: true,
            })
          },
          exitFullscreen: async () => {
            setStatusBarHidden(false, 'fade')
            setInFullsreen2(!inFullscreen2)
            await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT)
          },
        }}
        style={{
          videoBackgroundColor: 'black',
          height: inFullscreen2 ? Dimensions.get('window').width : 160,
          width: inFullscreen2 ? Dimensions.get('window').height : 320,
        }}
      />

      <Text style={styles.text}>Custom title</Text>
      <VideoPlayer
        videoProps={{
          shouldPlay: false,
          resizeMode: ResizeMode.CONTAIN,
          source: {
            uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
          },
        }}
        style={{
          videoBackgroundColor: 'black',
        }}
        header={<Text style={{ color: '#FFF' }}>Custom title</Text>}
      />
    </ScrollView>
  )
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#FFF',
    flex: 1,
  },
  contentContainer: {
    alignItems: 'center',
    justifyContent: 'center',
    paddingTop: 40,
  },
  text: {
    marginTop: 36,
    marginBottom: 12,
  },
})

export default App


================================================
FILE: example-app/app.json
================================================
{
  "expo": {
    "name": "example-app",
    "slug": "example-app",
    "privacy": "public",
    "platforms": [
      "ios",
      "android",
      "web"
    ],
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true
    }
  }
}


================================================
FILE: example-app/babel.config.js
================================================
module.exports = function (api) {
  api.cache(true)
  return {
    presets: ['babel-preset-expo'],
  }
}


================================================
FILE: example-app/package.json
================================================
{
    "private": true,
    "main": "^node_modules/expo/AppEntry.js",
    "scripts": {
        "start": "^expo start",
        "android": "^expo start --android",
        "ios": "^expo start --ios",
        "web": "^expo start --web",
        "eject": "^expo eject"
    },
    "dependencies": {
        "@react-native-community/slider": "4.2.3",
        "expo": "^46.0.13",
        "expo-av": "~12.0.4",
        "expo-screen-orientation": "~4.3.0",
        "expo-status-bar": "~1.4.0",
        "expo-video-player": "^2.1.0",
        "react": "18.0.0",
        "react-dom": "18.0.0",
        "react-native": "0.69.5",
        "react-native-screens": "~3.15.0",
        "react-native-web": "~0.18.7"
    },
    "devDependencies": {
        "@babel/core": "^7.18.6",
        "@types/react": "~18.0.0",
        "@types/react-native": "~0.69.1",
        "babel-preset-expo": "~9.2.0",
        "typescript": "^4.6.3"
    }
}


================================================
FILE: example-app/tsconfig.json
================================================
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "jsx": "react-native",
    "lib": [
      "dom",
      "esnext"
    ],
    "moduleResolution": "node",
    "noEmit": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "extends": "expo/tsconfig.base"
}


================================================
FILE: lib/constants.tsx
================================================
export enum ControlStates {
  Visible = 'Visible',
  Hidden = 'Hidden',
}

export enum PlaybackStates {
  Loading = 'Loading',
  Playing = 'Playing',
  Paused = 'Paused',
  Buffering = 'Buffering',
  Error = 'Error',
  Ended = 'Ended',
}

export enum ErrorSeverity {
  Fatal = 'Fatal',
  NonFatal = 'NonFatal',
}

export type ErrorType = {
  type: ErrorSeverity
  message: string
  obj: Record<string, unknown>
}


================================================
FILE: lib/index.tsx
================================================
import { AVPlaybackStatus, Audio, Video } from 'expo-av'
import {
  ActivityIndicator,
  Animated,
  StyleSheet,
  Text,
  TouchableWithoutFeedback,
  View,
} from 'react-native'
import { ControlStates, ErrorSeverity, PlaybackStates } from './constants'
import {
  ErrorMessage,
  TouchableButton,
  deepMerge,
  getMinutesSecondsFromMilliseconds,
  styles,
} from './utils'
import { MaterialIcons } from '@expo/vector-icons'
import { Props, defaultProps } from './props'
import { useEffect, useRef, useState } from 'react'
import React from 'react'
import Slider from '@react-native-community/slider'

const VideoPlayer = (tempProps: Props) => {
  const props = deepMerge(defaultProps, tempProps) as Props

  let playbackInstance: Video | null = null
  let controlsTimer: NodeJS.Timeout | null = null
  let initialShow = props.defaultControlsVisible
  const header = props.header

  const [errorMessage, setErrorMessage] = useState('')
  const controlsOpacity = useRef(new Animated.Value(props.defaultControlsVisible ? 1 : 0)).current
  const [controlsState, setControlsState] = useState(
    props.defaultControlsVisible ? ControlStates.Visible : ControlStates.Hidden
  )
  const [playbackInstanceInfo, setPlaybackInstanceInfo] = useState({
    position: 0,
    duration: 0,
    state: props.videoProps.source ? PlaybackStates.Loading : PlaybackStates.Error,
  })

  // We need to extract ref, because of misstypes in <Slider />
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const { ref: sliderRef, ...sliderProps } = props.slider
  const screenRatio = props.style.width! / props.style.height!

  let videoHeight = props.style.height
  let videoWidth = videoHeight! * screenRatio

  if (videoWidth > props.style.width!) {
    videoWidth = props.style.width!
    videoHeight = videoWidth / screenRatio
  }

  useEffect(() => {
    setAudio()

    return () => {
      if (playbackInstance) {
        playbackInstance.setStatusAsync({
          shouldPlay: false,
        })
      }
    }
  }, [])

  useEffect(() => {
    if (!props.videoProps.source) {
      console.error(
        '[VideoPlayer] `Source` is a required in `videoProps`. ' +
          'Check https://docs.expo.io/versions/latest/sdk/video/#usage'
      )
      setErrorMessage('`Source` is a required in `videoProps`')
      setPlaybackInstanceInfo({ ...playbackInstanceInfo, state: PlaybackStates.Error })
    } else {
      setPlaybackInstanceInfo({ ...playbackInstanceInfo, state: PlaybackStates.Playing })
    }
  }, [props.videoProps.source])

  const hideAnimation = () => {
    Animated.timing(controlsOpacity, {
      toValue: 0,
      duration: props.animation.fadeOutDuration,
      useNativeDriver: true,
    }).start(({ finished }) => {
      if (finished) {
        setControlsState(ControlStates.Hidden)
      }
    })
  }

  const animationToggle = () => {
    if (controlsState === ControlStates.Hidden) {
      Animated.timing(controlsOpacity, {
        toValue: 1,
        duration: props.animation.fadeInDuration,
        useNativeDriver: true,
      }).start(({ finished }) => {
        if (finished) {
          setControlsState(ControlStates.Visible)
        }
      })
    } else if (controlsState === ControlStates.Visible) {
      hideAnimation()
    }

    if (controlsTimer === null && props.autoHidePlayer) {
      controlsTimer = setTimeout(() => {
        if (
          playbackInstanceInfo.state === PlaybackStates.Playing &&
          controlsState === ControlStates.Hidden
        ) {
          hideAnimation()
        }
        if (controlsTimer) {
          clearTimeout(controlsTimer)
        }
        controlsTimer = null
      }, 2000)
    }
  }

  // Set audio mode to play even in silent mode (like the YouTube app)
  const setAudio = async () => {
    try {
      await Audio.setAudioModeAsync({
        playsInSilentModeIOS: true,
      })
    } catch (e) {
      props.errorCallback({
        type: ErrorSeverity.NonFatal,
        message: 'Audio.setAudioModeAsync',
        obj: e as Record<string, unknown>,
      })
    }
  }

  const updatePlaybackCallback = (status: AVPlaybackStatus) => {
    props.playbackCallback(status)

    if (status.isLoaded) {
      setPlaybackInstanceInfo({
        ...playbackInstanceInfo,
        position: status.positionMillis,
        duration: status.durationMillis || 0,
        state:
          status.positionMillis === status.durationMillis
            ? PlaybackStates.Ended
            : status.isBuffering
            ? PlaybackStates.Buffering
            : status.shouldPlay
            ? PlaybackStates.Playing
            : PlaybackStates.Paused,
      })
      if (
        (status.didJustFinish && controlsState === ControlStates.Hidden) ||
        (status.isBuffering && controlsState === ControlStates.Hidden && initialShow)
      ) {
        animationToggle()
        initialShow = false
      }
    } else {
      if (status.isLoaded === false && status.error) {
        const errorMsg = `Encountered a fatal error during playback: ${status.error}`
        setErrorMessage(errorMsg)
        props.errorCallback({ type: ErrorSeverity.Fatal, message: errorMsg, obj: {} })
      }
    }
  }

  const togglePlay = async () => {
    if (controlsState === ControlStates.Hidden) {
      return
    }
    const shouldPlay = playbackInstanceInfo.state !== PlaybackStates.Playing
    if (playbackInstance !== null) {
      await playbackInstance.setStatusAsync({
        shouldPlay,
        ...(playbackInstanceInfo.state === PlaybackStates.Ended && { positionMillis: 0 }),
      })
      setPlaybackInstanceInfo({
        ...playbackInstanceInfo,
        state:
          playbackInstanceInfo.state === PlaybackStates.Playing
            ? PlaybackStates.Paused
            : PlaybackStates.Playing,
      })
      if (shouldPlay) {
        animationToggle()
      }
    }
  }

  if (playbackInstanceInfo.state === PlaybackStates.Error) {
    return (
      <View
        style={{
          backgroundColor: props.style.videoBackgroundColor,
          width: videoWidth,
          height: videoHeight,
        }}
      >
        <ErrorMessage style={props.textStyle} message={errorMessage} />
      </View>
    )
  }

  if (playbackInstanceInfo.state === PlaybackStates.Loading) {
    return (
      <View
        style={{
          backgroundColor: props.style.controlsBackgroundColor,
          width: videoWidth,
          height: videoHeight,
          justifyContent: 'center',
        }}
      >
        {props.icon.loading || <ActivityIndicator {...props.activityIndicator} />}
      </View>
    )
  }

  return (
    <View
      style={{
        backgroundColor: props.style.videoBackgroundColor,
        width: videoWidth,
        height: videoHeight,
        maxWidth: '100%',
      }}
    >
      <Video
        style={styles.videoWrapper}
        {...props.videoProps}
        ref={component => {
          playbackInstance = component
          if (props.videoProps.ref) {
            props.videoProps.ref.current = component as Video
          }
        }}
        onPlaybackStatusUpdate={updatePlaybackCallback}
      />

      <Animated.View
        pointerEvents={controlsState === ControlStates.Visible ? 'auto' : 'none'}
        style={[
          styles.topInfoWrapper,
          {
            opacity: controlsOpacity,
          },
        ]}
      >
        {header}
      </Animated.View>

      <TouchableWithoutFeedback onPress={animationToggle}>
        <Animated.View
          style={{
            ...StyleSheet.absoluteFillObject,
            opacity: controlsOpacity,
            justifyContent: 'center',
            alignItems: 'center',
          }}
        >
          <View
            style={{
              ...StyleSheet.absoluteFillObject,
              backgroundColor: props.style.controlsBackgroundColor,
              opacity: 0.5,
            }}
          />
          <View pointerEvents={controlsState === ControlStates.Visible ? 'auto' : 'none'}>
            <View style={styles.iconWrapper}>
              <TouchableButton onPress={togglePlay}>
                <View>
                  {playbackInstanceInfo.state === PlaybackStates.Buffering &&
                    (props.icon.loading || <ActivityIndicator {...props.activityIndicator} />)}
                  {playbackInstanceInfo.state === PlaybackStates.Playing && props.icon.pause}
                  {playbackInstanceInfo.state === PlaybackStates.Paused && props.icon.play}
                  {playbackInstanceInfo.state === PlaybackStates.Ended && props.icon.replay}
                  {((playbackInstanceInfo.state === PlaybackStates.Ended && !props.icon.replay) ||
                    (playbackInstanceInfo.state === PlaybackStates.Playing && !props.icon.pause) ||
                    (playbackInstanceInfo.state === PlaybackStates.Paused &&
                      !props.icon.pause)) && (
                    <MaterialIcons
                      name={
                        playbackInstanceInfo.state === PlaybackStates.Playing
                          ? 'pause'
                          : playbackInstanceInfo.state === PlaybackStates.Paused
                          ? 'play-arrow'
                          : 'replay'
                      }
                      style={props.icon.style}
                      size={props.icon.size}
                      color={props.icon.color}
                    />
                  )}
                </View>
              </TouchableButton>
            </View>
          </View>
        </Animated.View>
      </TouchableWithoutFeedback>

      <Animated.View
          pointerEvents={controlsState === ControlStates.Visible ? 'auto' : 'none'}
          style={[
          styles.bottomInfoWrapper,
          {
            opacity: controlsOpacity,
          },
        ]}
      >
        {props.timeVisible && (
          <Text style={[props.textStyle, styles.timeLeft]}>
            {getMinutesSecondsFromMilliseconds(playbackInstanceInfo.position)}
          </Text>
        )}
        {props.slider.visible && (
          <Slider
            {...sliderProps}
            style={[styles.slider, props.slider.style]}
            value={
              playbackInstanceInfo.duration
                ? playbackInstanceInfo.position / playbackInstanceInfo.duration
                : 0
            }
            onSlidingStart={() => {
              if (playbackInstanceInfo.state === PlaybackStates.Playing) {
                togglePlay()
                setPlaybackInstanceInfo({ ...playbackInstanceInfo, state: PlaybackStates.Paused })
              }
            }}
            onSlidingComplete={async e => {
              const position = e * playbackInstanceInfo.duration
              if (playbackInstance) {
                await playbackInstance.setStatusAsync({
                  positionMillis: position,
                  shouldPlay: true,
                })
              }
              setPlaybackInstanceInfo({
                ...playbackInstanceInfo,
                position,
              })
            }}
          />
        )}
        {props.timeVisible && (
          <Text style={[props.textStyle, styles.timeRight]}>
            {getMinutesSecondsFromMilliseconds(playbackInstanceInfo.duration)}
          </Text>
        )}
        {props.mute.visible && (
          <TouchableButton
            onPress={() => (props.mute.isMute ? props.mute.exitMute?.() : props.mute.enterMute?.())}
          >
            <View>
              {props.icon.mute}
              {props.icon.exitMute}
              {((!props.icon.mute && props.mute.isMute) ||
                (!props.icon.exitMute && !props.mute.isMute)) && (
                <MaterialIcons
                  name={props.mute.isMute ? 'volume-up' : 'volume-off'}
                  style={props.icon.style}
                  size={props.icon.size! / 2}
                  color={props.icon.color}
                />
              )}
            </View>
          </TouchableButton>
        )}
        {props.fullscreen.visible && (
          <TouchableButton
            onPress={() =>
              props.fullscreen.inFullscreen
                ? props.fullscreen.exitFullscreen!()
                : props.fullscreen.enterFullscreen!()
            }
          >
            <View>
              {!props.fullscreen.inFullscreen && props.icon.fullscreen}
              {props.fullscreen.inFullscreen && props.icon.exitFullscreen}
              {((!props.icon.fullscreen && !props.fullscreen.inFullscreen) ||
                (!props.icon.exitFullscreen && props.fullscreen.inFullscreen)) && (
                <MaterialIcons
                  name={props.fullscreen.inFullscreen ? 'fullscreen-exit' : 'fullscreen'}
                  style={props.icon.style}
                  size={props.icon.size! / 2}
                  color={props.icon.color}
                />
              )}
            </View>
          </TouchableButton>
        )}
      </Animated.View>
    </View>
  )
}

VideoPlayer.defaultProps = defaultProps

export default VideoPlayer


================================================
FILE: lib/props.tsx
================================================
import { AVPlaybackStatus, Video, VideoProps } from 'expo-av'
import { ActivityIndicatorProps, Dimensions, Platform, TextStyle } from 'react-native'
import { ColorValue } from 'react-native'
import { ErrorType } from './constants'
import { MutableRefObject, ReactNode } from 'react'
import { SliderProps } from '@react-native-community/slider'

// https://github.com/typescript-cheatsheets/react/issues/415
export type Props = RequiredProps & DefaultProps

export const defaultProps = {
  errorCallback: error =>
    console.error(`[VideoPlayer] ${error.type} Error - ${error.message}: ${error.obj}`),
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  playbackCallback: () => {},
  defaultControlsVisible: false,
  timeVisible: true,
  slider: {
    visible: true,
  },
  textStyle: {
    color: '#FFF',
    fontSize: 12,
    textAlign: 'center',
  },
  activityIndicator: {
    size: 'large',
    color: '#999',
  },
  animation: {
    fadeInDuration: 300,
    fadeOutDuration: 300,
  },
  style: {
    width: Platform.OS === 'web' ? '100%' : Dimensions.get('window').width,
    height: Dimensions.get('window').height,
    videoBackgroundColor: '#000',
    controlsBackgroundColor: '#000',
  },
  icon: {
    size: 48,
    color: '#FFF',
    style: {
      padding: 2,
    },
  },
  fullscreen: {
    enterFullscreen: () =>
      // eslint-disable-next-line no-console
      console.log('[VideoPlayer] - missing `enterFullscreen` function in `fullscreen` prop'),
    exitFullscreen: () =>
      // eslint-disable-next-line no-console
      console.log('[VideoPlayer] - missing `exitFullscreen` function in `fullscreen` prop'),
    inFullscreen: false,
    visible: true,
  },
  autoHidePlayer: true,
  header: undefined,
  mute: {
    enterMute: () =>
      // eslint-disable-next-line no-console
      console.log('[VideoPlayer] - missing `enterMute` function in `mute` prop'),
    exitMute: () =>
      // eslint-disable-next-line no-console
      console.log('[VideoPlayer] - missing `exitMute` function in `mute` prop'),
    isMute: false,
    visible: false,
  },
} as DefaultProps

type RequiredProps = {
  videoProps: VideoProps & {
    ref?: MutableRefObject<Video>
  }
}

type DefaultProps = {
  errorCallback: (error: ErrorType) => void
  playbackCallback: (status: AVPlaybackStatus) => void
  defaultControlsVisible: boolean
  timeVisible: boolean
  textStyle: TextStyle
  slider: {
    visible?: boolean
  } & SliderProps
  activityIndicator: ActivityIndicatorProps
  animation: {
    fadeInDuration?: number
    fadeOutDuration?: number
  }
  header: ReactNode
  style: {
    width?: number
    height?: number
    videoBackgroundColor?: ColorValue
    controlsBackgroundColor?: ColorValue
  }
  icon: {
    size?: number
    color?: ColorValue
    style?: TextStyle
    pause?: JSX.Element
    play?: JSX.Element
    replay?: JSX.Element
    loading?: JSX.Element
    fullscreen?: JSX.Element
    exitFullscreen?: JSX.Element
    mute?: JSX.Element
    exitMute?: JSX.Element
  }
  fullscreen: {
    enterFullscreen?: () => void
    exitFullscreen?: () => void
    inFullscreen?: boolean
    visible?: boolean
  }
  autoHidePlayer: boolean
  mute: {
    enterMute?: () => void
    exitMute?: () => void
    isMute?: boolean
    visible?: boolean
  }
}


================================================
FILE: lib/utils.tsx
================================================
import {
  Platform,
  StyleSheet,
  Text,
  TextStyle,
  TouchableNativeFeedback,
  TouchableNativeFeedbackProps,
  TouchableOpacity,
  TouchableOpacityProps,
  View,
} from 'react-native'
import React from 'react'

export const ErrorMessage = ({ message, style }: { message: string; style: TextStyle }) => (
  <View style={styles.errorWrapper}>
    <Text style={style}>{message}</Text>
  </View>
)

export const getMinutesSecondsFromMilliseconds = (ms: number) => {
  const totalSeconds = ms / 1000
  const seconds = String(Math.floor(totalSeconds % 60))
  const minutes = String(Math.floor(totalSeconds / 60))

  return minutes.padStart(1, '0') + ':' + seconds.padStart(2, '0')
}

type ButtonProps = (TouchableNativeFeedbackProps | TouchableOpacityProps) & {
  children: React.ReactNode
}
export const TouchableButton = (props: ButtonProps) =>
  Platform.OS === 'android' ? (
    <TouchableNativeFeedback
      background={TouchableNativeFeedback.Ripple('white', true)}
      {...props}
    />
  ) : (
    <TouchableOpacity {...props} />
  )

// https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6#gistcomment-3585151
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const deepMerge = (target: { [x: string]: any }, source: { [x: string]: any }) => {
  const result = { ...target, ...source }
  const keys = Object.keys(result)

  for (const key of keys) {
    const tprop = target[key]
    const sprop = source[key]
    if (typeof tprop === 'object' && typeof sprop === 'object') {
      result[key] = deepMerge(tprop, sprop)
    }
  }

  return result
}
export const styles = StyleSheet.create({
  errorWrapper: {
    ...StyleSheet.absoluteFillObject,
    paddingHorizontal: 20,
    justifyContent: 'center',
  },
  videoWrapper: {
    flex: 1,
    justifyContent: 'center',
  },
  iconWrapper: {
    borderRadius: 100,
    overflow: 'hidden',
    padding: 10,
  },
  bottomInfoWrapper: {
    position: 'absolute',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    flex: 1,
    bottom: 0,
    left: 0,
    right: 0,
  },
  topInfoWrapper: {
    position: 'absolute',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    flex: 1,
    top: 0,
    left: 0,
    right: 0,
    zIndex: 999,
  },
  timeLeft: { backgroundColor: 'transparent', marginLeft: 5 },
  timeRight: { backgroundColor: 'transparent', marginRight: 5 },
  slider: { flex: 1, paddingHorizontal: 10 },
})


================================================
FILE: migration-1x-to-2x.md
================================================
# Updating to version 2.x

Some properties has been renamed, removed, but a lot of functionality has been added. Please check [README.md](https://github.com/ihmpavel/expo-video-player/blob/master/README.md)

Old property name | State | Current property name | Description
---- | :--: | :-----: | -----------
**debug** | ❌ | - | This prop has been removed
**videoProps** | ✔️ | **videoProps** | Not changed
**width** | ⚠️ | **style.width** | Property moved to the object `style`
**height** | ⚠️ | **style.height** | Property moved to the object `style`
**videoBackground** | ⚠️ | **style.videoBackground** | Property moved to the object `style`
**videoRef** | ⚠️ | **videoProps.ref** | Property moved to the object `videoProps`. See usage in the [example-app](https://github.com/ihmpavel/expo-video-player/blob/master/example-app/App.tsx)
**fadeInDuration** | ⚠️ | **animation.fadeInDuration** | Property moved to the object `animation`
**fadeOutDuration** | ⚠️ | **animation.fadeOutDuration** | Property moved to the object `animation`
**hideControlsTimerDuration** | ❌ | - | Prop has been removed
**quickFadeOutDuration** | ❌ | - | Prop has been removed
**errorCallback** | ✔️ | - | Not changed
**playbackCallback** | ✔️ | - | Not changed
**textStyle** | ✔️ | - | Not changed
**inFullscreen** | ⚠️ | **fullscreen.inFullscreen** | Property moved to the object `fullscreen`
**showFullscreenButton** | ⚠️ | **fullscreen.visible** | Property moved to the object `fullscreen`
**switchToLandscape** | ⚠️ | **fullscreen.enterFullscreen** | Property moved to the object `fullscreen`
**switchToPortrait** | ⚠️ | **fullscreen.exitFullscreen** | Property moved to the object `fullscreen`
**thumbImage** | ⚠️ | **slider.thumbImage** | Property moved to the object `slider`. You can use any of the props (except `ref`, `value`, `onSlidingStart` and `onSlidingComplete`) from [@react-native-community/slider](https://github.com/callstack/react-native-slider)
**iosTrackImage** | ⚠️ | **slider.trackImage** | Property moved to the object `slider`. You can use any of the props (except `ref`, `value`, `onSlidingStart` and `onSlidingComplete`) from [@react-native-community/slider](https://github.com/callstack/react-native-slider)
**sliderColor** | ⚠️ | **slider.minimumTrackTintColor** | Property moved to the object `slider`. You can use any of the props (except `ref`, `value`, `onSlidingStart` and `onSlidingComplete`) from [@react-native-community/slider](https://github.com/callstack/react-native-slider)
**disableSlider** | ⚠️ | **slider.visible** | Property moved to the object `slider`. You can use any of the props (except `ref`, `value`, `onSlidingStart` and `onSlidingComplete`) from [@react-native-community/slider](https://github.com/callstack/react-native-slider)
**showControlsOnLoad** | ⚠️ | **defaultControlsVisible** | Prop has been renamed
**fullscreenEnterIcon** | ⚠️ | **icon.fullscreenEnter** | Property moved to the object `icon`
**fullscreenExitIcon** | ⚠️ | **icon.fullscreenExit** | Property moved to the object `icon`
**playIcon** | ⚠️ | **icon.play** | Property moved to the object `icon`
**pauseIcon** | ⚠️ | **icon.pause** | Property moved to the object `icon`
**replayIcon** | ⚠️ | **icon.replay** | Property moved to the object `icon`
**spinner** | ⚠️ | **icon.loading** | Property moved to the object `icon`

## Guide
- ❌ - Property removed
- ⚠️ - Something changed
- ✔️ - Nothing changed


================================================
FILE: package.json
================================================
{
    "name": "expo-video-player",
    "version": "2.2.0",
    "private": false,
    "description": "Customizable Video Player controls for Expo",
    "keywords": [
        "customizable",
        "expo",
        "player",
        "react-native",
        "video-player",
        "expo-video-player",
        "videoplayer",
        "expo-videoplayer"
    ],
    "homepage": "https://github.com/ihmpavel/expo-video-player",
    "bugs": "https://github.com/ihmpavel/expo-video-player/issues",
    "repository": {
        "type": "git"
    },
    "license": "MIT",
    "author": "Pavel Ihm",
    "main": "dist/index.js",
    "types": "dist/index.d.ts",
    "scripts": {
        "build": "rm -rf dist && tsc",
        "lint": "eslint \"lib/**/*.{js,ts,tsx}\"",
        "lint:fix": "eslint \"lib/**/*.{js,ts,tsx}\" --fix"
    },
    "devDependencies": {
        "@react-native-community/slider": "^4.3.1",
        "@types/react": "^18.0.21",
        "@types/react-native": "^0.70.4",
        "@typescript-eslint/eslint-plugin": "^5.38.1",
        "@typescript-eslint/parser": "^5.38.1",
        "eslint": "^8.24.0",
        "eslint-config-prettier": "^8.5.0",
        "eslint-config-react-app": "^7.0.1",
        "eslint-plugin-import": "^2.26.0",
        "eslint-plugin-prettier": "^4.2.1",
        "eslint-plugin-react": "^7.31.8",
        "eslint-plugin-react-hooks": "^4.6.0",
        "eslint-plugin-react-native": "^4.0.0",
        "eslint-plugin-sort-imports-es6-autofix": "^0.6.0",
        "expo": "^46.0.13",
        "expo-av": "^12.0.4",
        "prettier": "^2.7.1",
        "react": "^18.2.0",
        "react-dom": "^18.2.0",
        "react-native": "^0.70.1",
        "typescript": "^4.8.4"
    },
    "peerDependencies": {
        "@react-native-community/slider": ">=4.0.0",
        "expo": ">=38.0.0",
        "expo-av": ">=5.0.2"
    },
    "dependencies": {
        "tslib": "^2.4.0"
    }
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es6",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "checkJs": true,
    "jsx": "react-native",
    "declaration": true,
    "outDir": "./dist",
    "importHelpers": true,
    "downlevelIteration": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noPropertyAccessFromIndexSignature": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "lib"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "example-app",
    "lib/**/__tests__/",
    "lib/setupTests.ts"
  ],
}
Download .txt
gitextract_337zigfa/

├── .eslintrc.js
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── dependabot.yml
├── .gitignore
├── .npmignore
├── .prettierrc.js
├── .vscode/
│   └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist/
│   ├── constants.d.ts
│   ├── constants.js
│   ├── index.d.ts
│   ├── index.js
│   ├── props.d.ts
│   ├── props.js
│   ├── utils.d.ts
│   └── utils.js
├── example-app/
│   ├── .expo-shared/
│   │   └── assets.json
│   ├── .gitignore
│   ├── App.tsx
│   ├── app.json
│   ├── babel.config.js
│   ├── package.json
│   └── tsconfig.json
├── lib/
│   ├── constants.tsx
│   ├── index.tsx
│   ├── props.tsx
│   └── utils.tsx
├── migration-1x-to-2x.md
├── package.json
└── tsconfig.json
Download .txt
SYMBOL INDEX (16 symbols across 6 files)

FILE: dist/constants.d.ts
  type ControlStates (line 1) | enum ControlStates {
  type PlaybackStates (line 5) | enum PlaybackStates {
  type ErrorSeverity (line 13) | enum ErrorSeverity {
  type ErrorType (line 17) | type ErrorType = {

FILE: dist/props.d.ts
  type Props (line 7) | type Props = RequiredProps & DefaultProps;
  type RequiredProps (line 9) | type RequiredProps = {
  type DefaultProps (line 14) | type DefaultProps = {

FILE: dist/utils.d.ts
  type ButtonProps (line 8) | type ButtonProps = (TouchableNativeFeedbackProps | TouchableOpacityProps...

FILE: lib/constants.tsx
  type ControlStates (line 1) | enum ControlStates {
  type PlaybackStates (line 6) | enum PlaybackStates {
  type ErrorSeverity (line 15) | enum ErrorSeverity {
  type ErrorType (line 20) | type ErrorType = {

FILE: lib/props.tsx
  type Props (line 9) | type Props = RequiredProps & DefaultProps
  type RequiredProps (line 71) | type RequiredProps = {
  type DefaultProps (line 77) | type DefaultProps = {

FILE: lib/utils.tsx
  type ButtonProps (line 28) | type ButtonProps = (TouchableNativeFeedbackProps | TouchableOpacityProps...
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (80K chars).
[
  {
    "path": ".eslintrc.js",
    "chars": 2139,
    "preview": "// https://robertcooper.me/post/using-eslint-and-prettier-in-a-typescript-project\nmodule.exports = {\n  parser: '@typescr"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 17,
    "preview": "github: ihmpavel\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 758,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 595,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 144,
    "preview": "version: 2\nupdates:\n- package-ecosystem: npm\n  directory: \"/\"\n  schedule:\n    interval: weekly\n    time: \"04:00\"\n  open-"
  },
  {
    "path": ".gitignore",
    "chars": 18,
    "preview": "node_modules/**/*\n"
  },
  {
    "path": ".npmignore",
    "chars": 93,
    "preview": "lib/\nexample-app/\n.vscode/\n.github/\n.eslintrc.js\n.prettierrc.js\ntsconfig.json\njest.config.js\n"
  },
  {
    "path": ".prettierrc.js",
    "chars": 180,
    "preview": "module.exports = {\n  semi: false,\n  useTabs: false,\n  trailingComma: \"es5\",\n  singleQuote: true,\n  printWidth: 100,\n  ta"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 741,
    "preview": "{\n    \"typescript.tsdk\": \"node_modules\\\\typescript\\\\lib\",\n    \"eslint.autoFixOnSave\": true,\n    \"eslint.validate\": [\n   "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 3509,
    "preview": "# ChangeLog\n\n## 2.2.0 (September 29, 2022)\n- Fix: Prevent accidental pressing of buttons in overlay header when the over"
  },
  {
    "path": "LICENSE",
    "chars": 1083,
    "preview": "MIT License\n\nCopyright (c) 2021 @ihmpavel/expo-video-player\n\nPermission is hereby granted, free of charge, to any person"
  },
  {
    "path": "README.md",
    "chars": 6860,
    "preview": "# Basic info\nVideo wrappper component for Expo ecosystem built on top of the Expo's [Video component](https://docs.expo."
  },
  {
    "path": "dist/constants.d.ts",
    "chars": 469,
    "preview": "export declare enum ControlStates {\n    Visible = \"Visible\",\n    Hidden = \"Hidden\"\n}\nexport declare enum PlaybackStates "
  },
  {
    "path": "dist/constants.js",
    "chars": 711,
    "preview": "export var ControlStates;\n(function (ControlStates) {\n    ControlStates[\"Visible\"] = \"Visible\";\n    ControlStates[\"Hidde"
  },
  {
    "path": "dist/index.d.ts",
    "chars": 2184,
    "preview": "import { AVPlaybackStatus } from 'expo-av';\nimport { Props } from './props';\nimport React from 'react';\ndeclare const Vi"
  },
  {
    "path": "dist/index.js",
    "chars": 13146,
    "preview": "import { __awaiter, __rest } from \"tslib\";\nimport { Audio, Video } from 'expo-av';\nimport { ActivityIndicator, Animated,"
  },
  {
    "path": "dist/props.d.ts",
    "chars": 1835,
    "preview": "import { AVPlaybackStatus, Video, VideoProps } from 'expo-av';\nimport { ActivityIndicatorProps, TextStyle } from 'react-"
  },
  {
    "path": "dist/props.js",
    "chars": 1854,
    "preview": "import { Dimensions, Platform } from 'react-native';\nexport const defaultProps = {\n    errorCallback: error => console.e"
  },
  {
    "path": "dist/utils.d.ts",
    "chars": 1857,
    "preview": "import { TextStyle, TouchableNativeFeedbackProps, TouchableOpacityProps } from 'react-native';\nimport React from 'react'"
  },
  {
    "path": "dist/utils.js",
    "chars": 2355,
    "preview": "import { Platform, StyleSheet, Text, TouchableNativeFeedback, TouchableOpacity, View, } from 'react-native';\nimport Reac"
  },
  {
    "path": "example-app/.expo-shared/assets.json",
    "chars": 154,
    "preview": "{\n  \"f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd\": true,\n  \"89ed26367cdb9b771858e026f2eb95bfdb90e5a"
  },
  {
    "path": "example-app/.gitignore",
    "chars": 130,
    "preview": "node_modules/**/*\n.expo/*\nnpm-debug.*\n*.jks\n*.p8\n*.p12\n*.key\n*.mobileprovision\n*.orig.*\nweb-build/\nweb-report/\n\n# macOS\n"
  },
  {
    "path": "example-app/App.tsx",
    "chars": 7382,
    "preview": "import * as ScreenOrientation from 'expo-screen-orientation'\nimport { Dimensions, ScrollView, StyleSheet, Text } from 'r"
  },
  {
    "path": "example-app/app.json",
    "chars": 537,
    "preview": "{\n  \"expo\": {\n    \"name\": \"example-app\",\n    \"slug\": \"example-app\",\n    \"privacy\": \"public\",\n    \"platforms\": [\n      \"i"
  },
  {
    "path": "example-app/babel.config.js",
    "chars": 105,
    "preview": "module.exports = function (api) {\n  api.cache(true)\n  return {\n    presets: ['babel-preset-expo'],\n  }\n}\n"
  },
  {
    "path": "example-app/package.json",
    "chars": 918,
    "preview": "{\n    \"private\": true,\n    \"main\": \"^node_modules/expo/AppEntry.js\",\n    \"scripts\": {\n        \"start\": \"^expo start\",\n  "
  },
  {
    "path": "example-app/tsconfig.json",
    "chars": 291,
    "preview": "{\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"jsx\": \"react-native\",\n    \"lib\": [\n      \"dom\",\n"
  },
  {
    "path": "lib/constants.tsx",
    "chars": 413,
    "preview": "export enum ControlStates {\n  Visible = 'Visible',\n  Hidden = 'Hidden',\n}\n\nexport enum PlaybackStates {\n  Loading = 'Loa"
  },
  {
    "path": "lib/index.tsx",
    "chars": 13053,
    "preview": "import { AVPlaybackStatus, Audio, Video } from 'expo-av'\nimport {\n  ActivityIndicator,\n  Animated,\n  StyleSheet,\n  Text,"
  },
  {
    "path": "lib/props.tsx",
    "chars": 3287,
    "preview": "import { AVPlaybackStatus, Video, VideoProps } from 'expo-av'\nimport { ActivityIndicatorProps, Dimensions, Platform, Tex"
  },
  {
    "path": "lib/utils.tsx",
    "chars": 2487,
    "preview": "import {\n  Platform,\n  StyleSheet,\n  Text,\n  TextStyle,\n  TouchableNativeFeedback,\n  TouchableNativeFeedbackProps,\n  Tou"
  },
  {
    "path": "migration-1x-to-2x.md",
    "chars": 3409,
    "preview": "# Updating to version 2.x\n\nSome properties has been renamed, removed, but a lot of functionality has been added. Please "
  },
  {
    "path": "package.json",
    "chars": 1903,
    "preview": "{\n    \"name\": \"expo-video-player\",\n    \"version\": \"2.2.0\",\n    \"private\": false,\n    \"description\": \"Customizable Video "
  },
  {
    "path": "tsconfig.json",
    "chars": 915,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es6\",\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"lib\": [\n     "
  }
]

About this extraction

This page contains the full source code of the ihmpavel/expo-video-player GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (73.8 KB), approximately 18.8k tokens, and a symbol index with 16 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!