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"
],
}
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
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.