diff --git a/.eslintrc b/.eslintrc
index 24a2055d..e67012ec 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,3 +1,10 @@
{
- "extends": "@react-native-community",
+ "extends": [
+ "@react-native",
+ "eslint:recommended",
+ "plugin:react/recommended"
+ ],
+ "parserOptions": {
+ "requireConfigFile": false
+ }
}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ec7299e4..468c5880 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,4 +54,9 @@ android/keystores/debug.keystore
# windows
Deploy.binlog
msbuild.binlog
-android/buildOutput_*
\ No newline at end of file
+android/buildOutput_*
+
+# lib build
+lib/
+!src/lib
+*.tsbuildinfo
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
index 1d4c3eff..2a3043bd 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,8 +1,6 @@
{
- "requirePragma": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": false,
- "jsxBracketSameLine": true,
- "parser": "flow"
+ "jsxBracketSameLine": true
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d87c2de0..f09b85ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
## Changelog
## Next
+- All: add built-in typescript support [#3266](https://github.com/react-native-video/react-native-video/pull/3266)
- iOS, Android: expose playback functions to ref [#3245](https://github.com/react-native-video/react-native-video/pull/3245)
- Windows: fix build error from over-specified SDK version [#3246](https://github.com/react-native-video/react-native-video/pull/3246)
- Windows: fix `onError` not being raised [#3247](https://github.com/react-native-video/react-native-video/pull/3247)
diff --git a/Video.js b/Video.js
deleted file mode 100644
index c6abf501..00000000
--- a/Video.js
+++ /dev/null
@@ -1,587 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { StyleSheet, requireNativeComponent, NativeModules, UIManager, View, Image, Platform, findNodeHandle } from 'react-native';
-import { ViewPropTypes, ImagePropTypes } from 'deprecated-react-native-prop-types';
-import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
-import TextTrackType from './TextTrackType';
-import FilterType from './FilterType';
-import DRMType from './DRMType';
-import VideoResizeMode from './VideoResizeMode.js';
-
-const styles = StyleSheet.create({
- base: {
- overflow: 'hidden',
- },
-});
-
-const { VideoDecoderProperties } = NativeModules;
-export { TextTrackType, FilterType, DRMType, VideoDecoderProperties };
-
-export default class Video extends Component {
-
- constructor(props) {
- super(props);
-
- this.state = {
- showPoster: !!props.poster,
- };
- }
-
- setNativeProps(nativeProps) {
- this._root.setNativeProps(nativeProps);
- }
-
- toTypeString(x) {
- switch (typeof x) {
- case 'object':
- return x instanceof Date
- ? x.toISOString()
- : JSON.stringify(x); // object, null
- case 'undefined':
- return '';
- default: // boolean, number, string
- return x.toString();
- }
- }
-
- stringsOnlyObject(obj) {
- const strObj = {};
-
- Object.keys(obj).forEach(x => {
- strObj[x] = this.toTypeString(obj[x]);
- });
-
- return strObj;
- }
-
- seek = (time, tolerance = 100) => {
- if (isNaN(time)) {throw new Error('Specified time is not a number');}
-
- if (Platform.OS === 'ios') {
- this.setNativeProps({
- seek: {
- time,
- tolerance,
- },
- });
- } else {
- this.setNativeProps({ seek: time });
- }
- };
-
- presentFullscreenPlayer = () => {
- this.setNativeProps({ fullscreen: true });
- };
-
- dismissFullscreenPlayer = () => {
- this.setNativeProps({ fullscreen: false });
- };
-
- save = async (options) => {
- return await NativeModules.VideoManager.save(options, findNodeHandle(this._root));
- }
-
- pause = async () => {
- await this.setPlayerPauseState(true);
- };
-
- play = async () => {
- await this.setPlayerPauseState(false);
- };
-
- setPlayerPauseState = async (paused) => {
- return await NativeModules.VideoManager.setPlayerPauseState(paused, findNodeHandle(this._root));
- };
-
- restoreUserInterfaceForPictureInPictureStopCompleted = (restored) => {
- this.setNativeProps({ restoreUserInterfaceForPIPStopCompletionHandler: restored });
- };
-
- _assignRoot = (component) => {
- this._root = component;
- };
-
- _hidePoster = () => {
- if (this.state.showPoster) {
- this.setState({ showPoster: false });
- }
- }
-
- _onLoadStart = (event) => {
- if (this.props.onLoadStart) {
- this.props.onLoadStart(event.nativeEvent);
- }
- };
-
- _onPlaybackStateChanged = (event) => {
- if (this.props.onPlaybackStateChanged) {
- this.props.onPlaybackStateChanged(event.nativeEvent);
- }
- };
-
- _onLoad = (event) => {
- // Need to hide poster here for windows as onReadyForDisplay is not implemented
- if (Platform.OS === 'windows') {
- this._hidePoster();
- }
- if (this.props.onLoad) {
- this.props.onLoad(event.nativeEvent);
- }
- };
-
- _onAudioTracks = (event) => {
- if (this.props.onAudioTracks) {
- this.props.onAudioTracks(event.nativeEvent);
- }
- };
-
- _onTextTracks = (event) => {
- if (this.props.onTextTracks) {
- this.props.onTextTracks(event.nativeEvent);
- }
- };
-
- _onVideoTracks = (event) => {
- if (this.props.onVideoTracks) {
- this.props.onVideoTracks(event.nativeEvent);
- }
- };
-
- _onError = (event) => {
- if (this.props.onError) {
- this.props.onError(event.nativeEvent);
- }
- };
-
- _onProgress = (event) => {
- if (this.props.onProgress) {
- this.props.onProgress(event.nativeEvent);
- }
- };
-
- _onBandwidthUpdate = (event) => {
- if (this.props.onBandwidthUpdate) {
- this.props.onBandwidthUpdate(event.nativeEvent);
- }
- };
-
- _onSeek = (event) => {
- if (this.props.onSeek) {
- this.props.onSeek(event.nativeEvent);
- }
- };
-
- _onEnd = (event) => {
- if (this.props.onEnd) {
- this.props.onEnd(event.nativeEvent);
- }
- };
-
- _onTimedMetadata = (event) => {
- if (this.props.onTimedMetadata) {
- this.props.onTimedMetadata(event.nativeEvent);
- }
- };
-
- _onFullscreenPlayerWillPresent = (event) => {
- if (this.props.onFullscreenPlayerWillPresent) {
- this.props.onFullscreenPlayerWillPresent(event.nativeEvent);
- }
- };
-
- _onFullscreenPlayerDidPresent = (event) => {
- if (this.props.onFullscreenPlayerDidPresent) {
- this.props.onFullscreenPlayerDidPresent(event.nativeEvent);
- }
- };
-
- _onFullscreenPlayerWillDismiss = (event) => {
- if (this.props.onFullscreenPlayerWillDismiss) {
- this.props.onFullscreenPlayerWillDismiss(event.nativeEvent);
- }
- };
-
- _onFullscreenPlayerDidDismiss = (event) => {
- if (this.props.onFullscreenPlayerDidDismiss) {
- this.props.onFullscreenPlayerDidDismiss(event.nativeEvent);
- }
- };
-
- _onReadyForDisplay = (event) => {
- if (!this.props.audioOnly) {
- this._hidePoster();
- }
-
- if (this.props.onReadyForDisplay) {
- this.props.onReadyForDisplay(event.nativeEvent);
- }
- };
-
- _onPlaybackStalled = (event) => {
- if (this.props.onPlaybackStalled) {
- this.props.onPlaybackStalled(event.nativeEvent);
- }
- };
-
- _onPlaybackResume = (event) => {
- if (this.props.onPlaybackResume) {
- this.props.onPlaybackResume(event.nativeEvent);
- }
- };
-
- _onPlaybackRateChange = (event) => {
- if (this.props.onPlaybackRateChange) {
- this.props.onPlaybackRateChange(event.nativeEvent);
- }
- };
-
- _onExternalPlaybackChange = (event) => {
- if (this.props.onExternalPlaybackChange) {
- this.props.onExternalPlaybackChange(event.nativeEvent);
- }
- }
-
- _onAudioBecomingNoisy = () => {
- if (this.props.onAudioBecomingNoisy) {
- this.props.onAudioBecomingNoisy();
- }
- };
-
- _onPictureInPictureStatusChanged = (event) => {
- if (this.props.onPictureInPictureStatusChanged) {
- this.props.onPictureInPictureStatusChanged(event.nativeEvent);
- }
- };
-
- _onRestoreUserInterfaceForPictureInPictureStop = (event) => {
- if (this.props.onRestoreUserInterfaceForPictureInPictureStop) {
- this.props.onRestoreUserInterfaceForPictureInPictureStop();
- }
- };
-
- _onAudioFocusChanged = (event) => {
- if (this.props.onAudioFocusChanged) {
- this.props.onAudioFocusChanged(event.nativeEvent);
- }
- };
-
- _onBuffer = (event) => {
- if (this.props.onBuffer) {
- this.props.onBuffer(event.nativeEvent);
- }
- };
-
- _onGetLicense = (event) => {
- if (this.props.drm && this.props.drm.getLicense instanceof Function) {
- const data = event.nativeEvent;
- if (data && data.spcBase64) {
- const getLicenseOverride = this.props.drm.getLicense(data.spcBase64, data.contentId, data.licenseUrl);
- const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not.
- getLicensePromise.then((result => {
- if (result !== undefined) {
- NativeModules.VideoManager.setLicenseResult(result, data.licenseUrl, findNodeHandle(this._root));
- } else {
- NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('Empty license result', data.licenseUrl, findNodeHandle(this._root));
- }
- })).catch((error) => {
- NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, data.licenseUrl, findNodeHandle(this._root));
- });
- } else {
- NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('No spc received', data.licenseUrl, findNodeHandle(this._root));
- }
- }
- }
-
- _onReceiveAdEvent = (event) => {
- if (this.props.onReceiveAdEvent) {
- this.props.onReceiveAdEvent(event.nativeEvent);
- }
- };
-
- getViewManagerConfig = viewManagerName => {
- if (!UIManager.getViewManagerConfig) {
- return UIManager[viewManagerName];
- }
- return UIManager.getViewManagerConfig(viewManagerName);
- };
-
- render() {
- const resizeMode = this.props.resizeMode;
- const source = resolveAssetSource(this.props.source) || {};
- const shouldCache = !source.__packager_asset;
-
- let uri = source.uri || '';
- if (uri && uri.match(/^\//)) {
- uri = `file://${uri}`;
- }
-
- if (!uri) {
- console.log('Trying to load empty source.');
- }
-
- const isNetwork = !!(uri && uri.match(/^https?:/i));
- const isAsset = !!(uri && uri.match(/^(assets-library|ph|ipod-library|file|content|ms-appx|ms-appdata):/i));
-
- if ((uri || uri === '') && !isNetwork && !isAsset) {
- if (this.props.onError) {
- this.props.onError({error: {errorString: 'invalid url, player will stop', errorCode: 'INVALID_URL'}});
- }
- }
-
- let nativeResizeMode;
- const RCTVideoInstance = this.getViewManagerConfig('RCTVideo');
-
- if (resizeMode === VideoResizeMode.stretch) {
- nativeResizeMode = RCTVideoInstance.Constants.ScaleToFill;
- } else if (resizeMode === VideoResizeMode.contain) {
- nativeResizeMode = RCTVideoInstance.Constants.ScaleAspectFit;
- } else if (resizeMode === VideoResizeMode.cover) {
- nativeResizeMode = RCTVideoInstance.Constants.ScaleAspectFill;
- } else {
- nativeResizeMode = RCTVideoInstance.Constants.ScaleNone;
- }
-
- const nativeProps = Object.assign({}, this.props);
- Object.assign(nativeProps, {
- style: [styles.base, nativeProps.style],
- resizeMode: nativeResizeMode,
- src: {
- uri,
- isNetwork,
- isAsset,
- shouldCache,
- type: source.type || '',
- mainVer: source.mainVer || 0,
- patchVer: source.patchVer || 0,
- requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {},
- startTime: source.startTime || 0,
- endTime: source.endTime,
- // Custom Metadata
- title: source.title,
- subtitle: source.subtitle,
- description: source.description,
- },
- onVideoLoadStart: this._onLoadStart,
- onVideoPlaybackStateChanged: this._onPlaybackStateChanged,
- onVideoLoad: this._onLoad,
- onAudioTracks: this._onAudioTracks,
- onTextTracks: this._onTextTracks,
- onVideoTracks: this._onVideoTracks,
- onVideoError: this._onError,
- onVideoProgress: this._onProgress,
- onVideoSeek: this._onSeek,
- onVideoEnd: this._onEnd,
- onVideoBuffer: this._onBuffer,
- onVideoBandwidthUpdate: this._onBandwidthUpdate,
- onTimedMetadata: this._onTimedMetadata,
- onVideoAudioBecomingNoisy: this._onAudioBecomingNoisy,
- onVideoExternalPlaybackChange: this._onExternalPlaybackChange,
- onVideoFullscreenPlayerWillPresent: this._onFullscreenPlayerWillPresent,
- onVideoFullscreenPlayerDidPresent: this._onFullscreenPlayerDidPresent,
- onVideoFullscreenPlayerWillDismiss: this._onFullscreenPlayerWillDismiss,
- onVideoFullscreenPlayerDidDismiss: this._onFullscreenPlayerDidDismiss,
- onReadyForDisplay: this._onReadyForDisplay,
- onPlaybackStalled: this._onPlaybackStalled,
- onPlaybackResume: this._onPlaybackResume,
- onPlaybackRateChange: this._onPlaybackRateChange,
- onAudioFocusChanged: this._onAudioFocusChanged,
- onAudioBecomingNoisy: this._onAudioBecomingNoisy,
- onGetLicense: nativeProps.drm && nativeProps.drm.getLicense && this._onGetLicense,
- onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged,
- onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop,
- onReceiveAdEvent: this._onReceiveAdEvent,
- });
-
- const posterStyle = {
- ...StyleSheet.absoluteFillObject,
- resizeMode: this.props.posterResizeMode || 'contain',
- };
-
- return (
-
-
- {this.state.showPoster && (
-
- )}
-
- );
- }
-}
-
-Video.propTypes = {
- filter: PropTypes.oneOf([
- FilterType.NONE,
- FilterType.INVERT,
- FilterType.MONOCHROME,
- FilterType.POSTERIZE,
- FilterType.FALSE,
- FilterType.MAXIMUMCOMPONENT,
- FilterType.MINIMUMCOMPONENT,
- FilterType.CHROME,
- FilterType.FADE,
- FilterType.INSTANT,
- FilterType.MONO,
- FilterType.NOIR,
- FilterType.PROCESS,
- FilterType.TONAL,
- FilterType.TRANSFER,
- FilterType.SEPIA,
- ]),
- filterEnabled: PropTypes.bool,
- onVideoLoadStart: PropTypes.func,
- onVideoLoad: PropTypes.func,
- onVideoBuffer: PropTypes.func,
- onVideoError: PropTypes.func,
- onVideoProgress: PropTypes.func,
- onVideoBandwidthUpdate: PropTypes.func,
- onVideoSeek: PropTypes.func,
- onVideoEnd: PropTypes.func,
- onTimedMetadata: PropTypes.func,
- onVideoAudioBecomingNoisy: PropTypes.func,
- onVideoExternalPlaybackChange: PropTypes.func,
- onVideoFullscreenPlayerWillPresent: PropTypes.func,
- onVideoFullscreenPlayerDidPresent: PropTypes.func,
- onVideoFullscreenPlayerWillDismiss: PropTypes.func,
- onVideoFullscreenPlayerDidDismiss: PropTypes.func,
-
- /* Wrapper component */
- source: PropTypes.oneOfType([
- PropTypes.shape({
- uri: PropTypes.string,
- }),
- // Opaque type returned by require('./video.mp4')
- PropTypes.number,
- ]),
- drm: PropTypes.shape({
- type: PropTypes.oneOf([
- DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY,
- ]),
- licenseServer: PropTypes.string,
- headers: PropTypes.shape({}),
- base64Certificate: PropTypes.bool,
- certificateUrl: PropTypes.string,
- getLicense: PropTypes.func,
- }),
- localSourceEncryptionKeyScheme: PropTypes.string,
- minLoadRetryCount: PropTypes.number,
- maxBitRate: PropTypes.number,
- resizeMode: PropTypes.string,
- poster: PropTypes.string,
- posterResizeMode: ImagePropTypes.resizeMode,
- repeat: PropTypes.bool,
- automaticallyWaitsToMinimizeStalling: PropTypes.bool,
- allowsExternalPlayback: PropTypes.bool,
- selectedAudioTrack: PropTypes.shape({
- type: PropTypes.string.isRequired,
- value: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number,
- ]),
- }),
- selectedVideoTrack: PropTypes.shape({
- type: PropTypes.string.isRequired,
- value: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number,
- ]),
- }),
- selectedTextTrack: PropTypes.shape({
- type: PropTypes.string.isRequired,
- value: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number,
- ]),
- }),
- textTracks: PropTypes.arrayOf(
- PropTypes.shape({
- title: PropTypes.string,
- uri: PropTypes.string.isRequired,
- type: PropTypes.oneOf([
- TextTrackType.SRT,
- TextTrackType.TTML,
- TextTrackType.VTT,
- ]),
- language: PropTypes.string.isRequired,
- })
- ),
- chapters: PropTypes.arrayOf(
- PropTypes.shape({
- title: PropTypes.string,
- startTime: PropTypes.number.isRequired,
- endTime: PropTypes.number.isRequired,
- })
- ),
- paused: PropTypes.bool,
- muted: PropTypes.bool,
- volume: PropTypes.number,
- bufferConfig: PropTypes.shape({
- minBufferMs: PropTypes.number,
- maxBufferMs: PropTypes.number,
- bufferForPlaybackMs: PropTypes.number,
- bufferForPlaybackAfterRebufferMs: PropTypes.number,
- maxHeapAllocationPercent: PropTypes.number,
- }),
- rate: PropTypes.number,
- pictureInPicture: PropTypes.bool,
- playInBackground: PropTypes.bool,
- preferredForwardBufferDuration: PropTypes.number,
- playWhenInactive: PropTypes.bool,
- ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']),
- reportBandwidth: PropTypes.bool,
- contentStartTime: PropTypes.number,
- disableFocus: PropTypes.bool,
- focusable: PropTypes.bool,
- disableBuffering: PropTypes.bool,
- controls: PropTypes.bool,
- audioOnly: PropTypes.bool,
- audioOutput: PropTypes.oneOf(['earpiece', 'speaker']),
- fullscreenAutorotate: PropTypes.bool,
- fullscreenOrientation: PropTypes.oneOf(['all', 'landscape', 'portrait']),
- progressUpdateInterval: PropTypes.number,
- subtitleStyle: PropTypes.shape({
- paddingTop: PropTypes.number,
- paddingBottom: PropTypes.number,
- paddingLeft: PropTypes.number,
- paddingRight: PropTypes.number,
- fontSize: PropTypes.number,
- }),
- useTextureView: PropTypes.bool,
- useSecureView: PropTypes.bool,
- hideShutterView: PropTypes.bool,
- shutterColor: PropTypes.string,
- onLoadStart: PropTypes.func,
- onPlaybackStateChanged: PropTypes.func,
- onLoad: PropTypes.func,
- onAudioTracks: PropTypes.func,
- onTextTracks: PropTypes.func,
- onVideoTracks: PropTypes.func,
- onBuffer: PropTypes.func,
- onError: PropTypes.func,
- onProgress: PropTypes.func,
- onBandwidthUpdate: PropTypes.func,
- onSeek: PropTypes.func,
- onEnd: PropTypes.func,
- onFullscreenPlayerWillPresent: PropTypes.func,
- onFullscreenPlayerDidPresent: PropTypes.func,
- onFullscreenPlayerWillDismiss: PropTypes.func,
- onFullscreenPlayerDidDismiss: PropTypes.func,
- onReadyForDisplay: PropTypes.func,
- onPlaybackStalled: PropTypes.func,
- onPlaybackResume: PropTypes.func,
- onPlaybackRateChange: PropTypes.func,
- onAudioFocusChanged: PropTypes.func,
- onAudioBecomingNoisy: PropTypes.func,
- onPictureInPictureStatusChanged: PropTypes.func,
- onExternalPlaybackChange: PropTypes.func,
- adTagUrl: PropTypes.string,
- onReceiveAdEvent: PropTypes.func,
-
- /* Required by react-native */
- ...ViewPropTypes,
-};
-
-const RCTVideo = requireNativeComponent('RCTVideo');
diff --git a/VideoResizeMode.js b/VideoResizeMode.js
deleted file mode 100644
index 15a89773..00000000
--- a/VideoResizeMode.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import keyMirror from 'keymirror';
-
-export default keyMirror({
- contain: null,
- cover: null,
- stretch: null,
-});
diff --git a/package.json b/package.json
index 5f6cf469..85cf9569 100644
--- a/package.json
+++ b/package.json
@@ -2,8 +2,9 @@
"name": "react-native-video",
"version": "6.0.0-alpha.8",
"description": "A element for react-native",
- "main": "Video.js",
- "source": "Video.js",
+ "main": "lib/index",
+ "source": "src/index",
+ "react-native": "src/index",
"license": "MIT",
"author": "Community Contributors",
"homepage": "https://github.com/react-native-video/react-native-video#readme",
@@ -11,24 +12,32 @@
"type": "git",
"url": "git@github.com:react-native-video/react-native-video.git"
},
+ "resolutions": {
+ "@types/react": "~18.0.0"
+ },
"devDependencies": {
- "@react-native-community/eslint-config": "^0.0.5",
- "eslint": "^6.5.1",
- "react": "16.9.0",
- "react-native": "0.61.5",
- "react-native-windows": "^0.61.0-0"
- },
- "dependencies": {
- "deprecated-react-native-prop-types": "^2.2.0",
- "keymirror": "^0.1.1",
- "prop-types": "^15.7.2"
+ "@react-native/eslint-config": "^0.72.2",
+ "@types/jest": "^28.1.2",
+ "@types/react": "~18.0.0",
+ "@types/react-native": "0.72.3",
+ "@typescript-eslint/eslint-plugin": "^6.7.4",
+ "eslint": "^8.19.0",
+ "eslint-plugin-jest": "^27.4.2",
+ "jest": "^29.7.0",
+ "prettier": "^2.4.1",
+ "react": "18.2.0",
+ "react-native": "0.72.5",
+ "react-native-windows": "^0.61.0-0",
+ "typescript": "5.1.6"
},
+ "dependencies": {},
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"scripts": {
"lint": "yarn eslint .",
+ "build": "yarn tsc",
"xbasic": "yarn --cwd examples/basic",
"test": "echo no test available"
},
@@ -36,11 +45,13 @@
"android",
"ios",
"windows",
- "FilterType.js",
- "DRMType.js",
- "TextTrackType.js",
- "VideoResizeMode.js",
+ "src",
"react-native-video.podspec",
- "docs"
+ "docs",
+ "!android/build",
+ "!android/buildOutput_*",
+ "!android/local.properties",
+ "!ios/build",
+ "!**/*.tsbuildinfo"
]
}
diff --git a/src/Video.tsx b/src/Video.tsx
new file mode 100644
index 00000000..d7b3e43e
--- /dev/null
+++ b/src/Video.tsx
@@ -0,0 +1,449 @@
+import React, {
+ useState,
+ useCallback,
+ useMemo,
+ useRef,
+ forwardRef,
+ useImperativeHandle,
+ type ComponentRef,
+} from "react";
+import {
+ View,
+ StyleSheet,
+ Image,
+ Platform,
+} from "react-native";
+import NativeVideoComponent, { RCTVideoConstants } from "./VideoNativeComponent";
+import type { NativeVideoResizeMode, OnAudioFocusChangedData, OnAudioTracksData, OnPlaybackStateChangedData, OnTextTracksData, OnTimedMetadataData, OnVideoErrorData, OnVideoTracksData } from './VideoNativeComponent'
+
+import type { StyleProp, ImageStyle, NativeSyntheticEvent } from "react-native";
+import {
+ type VideoComponentType,
+ type OnLoadData,
+ type OnGetLicenseData,
+ type OnLoadStartData,
+ type OnProgressData,
+ type OnSeekData,
+ type OnPictureInPictureStatusChangedData,
+ type OnBandwidthUpdateData,
+ type OnBufferData,
+ type OnExternalPlaybackChangeData,
+ type OnReceiveAdEventData,
+ VideoManager,
+} from "./VideoNativeComponent";
+import type { ReactVideoProps } from "./types/video";
+import { getReactTag, resolveAssetSourceForVideo } from "./utils";
+
+export interface VideoRef {
+ seek: (time: number, tolerance?: number) => void;
+ resume: () => void;
+ pause: () => void;
+ presentFullscreenPlayer: () => void;
+ dismissFullscreenPlayer: () => void;
+ restoreUserInterfaceForPictureInPictureStopCompleted: (restore: boolean) => void;
+}
+
+const Video = forwardRef(
+ (
+ {
+ source,
+ style,
+ resizeMode,
+ posterResizeMode,
+ poster,
+ fullscreen,
+ drm,
+ textTracks,
+ selectedAudioTrack,
+ selectedTextTrack,
+ onLoadStart,
+ onLoad,
+ onError,
+ onProgress,
+ onSeek,
+ onEnd,
+ onBuffer,
+ onBandwidthUpdate,
+ onExternalPlaybackChange,
+ onFullscreenPlayerWillPresent,
+ onFullscreenPlayerDidPresent,
+ onFullscreenPlayerWillDismiss,
+ onFullscreenPlayerDidDismiss,
+ onReadyForDisplay,
+ onPlaybackRateChange,
+ onAudioBecomingNoisy,
+ onPictureInPictureStatusChanged,
+ onRestoreUserInterfaceForPictureInPictureStop,
+ onReceiveAdEvent,
+ onPlaybackStateChanged,
+ onAudioFocusChanged,
+ onIdle,
+ onTimedMetadata,
+ onAudioTracks,
+ onTextTracks,
+ onVideoTracks,
+ ...rest
+ },
+ ref
+ ) => {
+ const nativeRef = useRef>(null);
+ const [showPoster, setShowPoster] = useState(!!poster);
+ const [isFullscreen, setIsFullscreen] = useState(fullscreen);
+ const [
+ _restoreUserInterfaceForPIPStopCompletionHandler,
+ setRestoreUserInterfaceForPIPStopCompletionHandler,
+ ] = useState();
+
+ const posterStyle = useMemo>(
+ () => ({
+ ...StyleSheet.absoluteFillObject,
+ resizeMode:
+ posterResizeMode && posterResizeMode !== "none"
+ ? posterResizeMode
+ : "contain",
+ }),
+ [posterResizeMode]
+ );
+
+ const src = useMemo(() => {
+ if (!source) return undefined;
+
+ const resolvedSource = resolveAssetSourceForVideo(source);
+ let uri = resolvedSource.uri || "";
+ if (uri && uri.match(/^\//)) uri = `file://${uri}`;
+ if (!uri) console.warn("Trying to load empty source");
+ const isNetwork = !!(uri && uri.match(/^https?:/));
+ const isAsset = !!(
+ uri &&
+ uri.match(
+ /^(assets-library|ipod-library|file|content|ms-appx|ms-appdata):/
+ )
+ );
+
+ return {
+ uri,
+ isNetwork,
+ isAsset,
+ shouldCache: resolvedSource.shouldCache || false,
+ type: resolvedSource.type || "",
+ mainVer: resolvedSource.mainVer || 0,
+ patchVer: resolvedSource.patchVer || 0,
+ requestHeaders: resolvedSource?.headers || {},
+ startTime: resolvedSource.startTime || 0,
+ endTime: resolvedSource.endTime
+ };
+ }, [source]);
+
+ const _resizeMode: NativeVideoResizeMode = useMemo(() => {
+ switch (resizeMode) {
+ case "contain":
+ return RCTVideoConstants.ScaleAspectFit;
+ case "cover":
+ return RCTVideoConstants.ScaleAspectFill;
+ case "stretch":
+ return RCTVideoConstants.ScaleToFill;
+ default:
+ return RCTVideoConstants.ScaleNone;
+ }
+ }, [resizeMode]);
+
+ const _drm = useMemo(() => {
+ if (!drm) return;
+ return {
+ drmType: drm.type,
+ licenseServer: drm.licenseServer,
+ headers: drm.headers,
+ contentId: drm.contentId,
+ certificateUrl: drm.certificateUrl,
+ base64Certificate: drm.base64Certificate,
+ useExternalGetLicense: !!drm.getLicense,
+ };
+ }, [drm]);
+
+
+ const _selectedTextTrack = useMemo(() => {
+ if (!selectedTextTrack) return;
+ if (typeof selectedTextTrack?.value === 'number') return {
+ seletedTextType: selectedTextTrack?.type,
+ index: selectedTextTrack?.value,
+ }
+ return {
+ selectedTextType: selectedTextTrack?.type,
+ value: selectedTextTrack?.value,
+ }
+ }, [selectedTextTrack]);
+
+ const _selectedAudioTrack = useMemo(() => {
+ if (!selectedAudioTrack) return;
+ if (typeof selectedAudioTrack?.value === 'number') return {
+ selectedAudioType: selectedAudioTrack?.type,
+ index: selectedAudioTrack?.value,
+ }
+ return {
+ selectedAudioType: selectedAudioTrack?.type,
+ value: selectedAudioTrack?.value,
+ }
+ }, [selectedAudioTrack]);
+
+
+ const seek = useCallback(
+ async (time: number, tolerance?: number) => {
+ if (isNaN(time)) throw new Error("Specified time is not a number");
+
+ if (!nativeRef.current) {
+ console.warn("Video Component is not mounted");
+ return;
+ }
+
+ Platform.select({
+ ios: () => {
+ nativeRef.current?.setNativeProps({
+ seek: {
+ time,
+ tolerance,
+ },
+ });
+ },
+ default: () => {
+ nativeRef.current?.setNativeProps({
+ seek: time,
+ });
+ },
+ })();
+ },
+ []
+ );
+
+ const presentFullscreenPlayer = useCallback(() => {
+ setIsFullscreen(true);
+ }, [setIsFullscreen]);
+
+ const dismissFullscreenPlayer = useCallback(() => {
+ setIsFullscreen(false);
+ }, [setIsFullscreen]);
+
+ const save = useCallback(async () => {
+ await VideoManager.save(getReactTag(nativeRef));
+ }, []);
+
+ const pause = useCallback(async () => {
+ await VideoManager.setPlayerPauseState(true, getReactTag(nativeRef));
+ }, []);
+
+ const resume = useCallback(async () => {
+ await VideoManager.setPlayerPauseState(false, getReactTag(nativeRef));
+ }, []);
+
+ const restoreUserInterfaceForPictureInPictureStopCompleted = useCallback(
+ (restored: boolean) => {
+ setRestoreUserInterfaceForPIPStopCompletionHandler(restored);
+ },
+ [setRestoreUserInterfaceForPIPStopCompletionHandler]
+ );
+
+ const onVideoLoadStart = useCallback(
+ (e: NativeSyntheticEvent) => {
+ onLoadStart?.(e.nativeEvent);
+ },
+ [onLoadStart]
+ );
+
+ const onVideoLoad = useCallback(
+ (e: NativeSyntheticEvent) => {
+ if (Platform.OS === "windows") setShowPoster(false);
+ onLoad?.(e.nativeEvent);
+ },
+ [onLoad, setShowPoster]
+ );
+
+ const onVideoError = useCallback(
+ (e: NativeSyntheticEvent) => {
+ onError?.(e.nativeEvent);
+ },
+ [onError]
+ );
+
+ const onVideoProgress = useCallback(
+ (e: NativeSyntheticEvent) => {
+ onProgress?.(e.nativeEvent);
+ },
+ [onProgress]
+ );
+
+ const onVideoSeek = useCallback(
+ (e: NativeSyntheticEvent) => {
+ onSeek?.(e.nativeEvent);
+ },
+ [onSeek]
+ );
+
+ // android only
+ const onVideoPlaybackStateChanged = useCallback((e: NativeSyntheticEvent) => {
+ onPlaybackStateChanged?.(e.nativeEvent);
+ }, [onPlaybackStateChanged])
+
+ // android only
+ const onVideoIdle = useCallback(() => {
+ onIdle?.()
+ }, [onIdle])
+
+ const _onTimedMetadata = useCallback(
+ (e: NativeSyntheticEvent) => {
+ onTimedMetadata?.(e.nativeEvent);
+ },
+ [onTimedMetadata]
+ );
+
+ const _onAudioTracks = useCallback((e: NativeSyntheticEvent) => {
+ onAudioTracks?.(e.nativeEvent)
+ }, [onAudioTracks])
+
+ const _onTextTracks = useCallback((e: NativeSyntheticEvent) => {
+ onTextTracks?.(e.nativeEvent)
+ }, [onTextTracks])
+
+ const _onVideoTracks = useCallback((e: NativeSyntheticEvent) => {
+ onVideoTracks?.(e.nativeEvent)
+ }, [onVideoTracks])
+
+ const _onPlaybackRateChange = useCallback(
+ (e: NativeSyntheticEvent>) => {
+ onPlaybackRateChange?.(e.nativeEvent);
+ },
+ [onPlaybackRateChange]
+ );
+
+ const _onReadyForDisplay = useCallback(() => {
+ setShowPoster(false);
+ onReadyForDisplay?.();
+ }, [setShowPoster, onReadyForDisplay]);
+
+ const _onPictureInPictureStatusChanged = useCallback(
+ (e: NativeSyntheticEvent) => {
+ onPictureInPictureStatusChanged?.(e.nativeEvent);
+ },
+ [onPictureInPictureStatusChanged]
+ );
+
+ const _onAudioFocusChanged = useCallback((e: NativeSyntheticEvent) => {
+ onAudioFocusChanged?.(e.nativeEvent)
+ }, [onAudioFocusChanged])
+
+ const onVideoBuffer = useCallback((e: NativeSyntheticEvent) => {
+ onBuffer?.(e.nativeEvent);
+ }, [onBuffer]);
+
+ const onVideoExternalPlaybackChange = useCallback((e: NativeSyntheticEvent) => {
+ onExternalPlaybackChange?.(e.nativeEvent);
+ }, [onExternalPlaybackChange])
+
+ const _onBandwidthUpdate = useCallback((e: NativeSyntheticEvent) => {
+ onBandwidthUpdate?.(e.nativeEvent);
+ }, [onBandwidthUpdate]);
+
+ const _onReceiveAdEvent = useCallback((e: NativeSyntheticEvent) => {
+ onReceiveAdEvent?.(e.nativeEvent);
+ }, [onReceiveAdEvent]);
+
+ const onGetLicense = useCallback(
+ (event: NativeSyntheticEvent) => {
+ if (drm && drm.getLicense instanceof Function) {
+ const data = event.nativeEvent;
+ if (data && data.spcBase64) {
+ const getLicenseOverride = drm.getLicense(data.spcBase64, data.contentId, data.licenseUrl);
+ const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not.
+ getLicensePromise.then((result => {
+ if (result !== undefined) {
+ nativeRef.current && VideoManager.setLicenseResult(result, data.licenseUrl, getReactTag(nativeRef));
+ } else {
+ nativeRef.current && VideoManager.setLicenseResultError('Empty license result', data.licenseUrl, getReactTag(nativeRef));
+ }
+ })).catch(() => {
+ nativeRef.current && VideoManager.setLicenseResultError('fetch error', data.licenseUrl, getReactTag(nativeRef));
+ });
+ } else {
+ VideoManager.setLicenseResultError('No spc received', data.licenseUrl, getReactTag(nativeRef));
+ }
+ }
+ },
+ [drm]
+ );
+
+ useImperativeHandle(
+ ref,
+ () => ({
+ seek,
+ presentFullscreenPlayer,
+ dismissFullscreenPlayer,
+ save,
+ pause,
+ resume,
+ restoreUserInterfaceForPictureInPictureStopCompleted,
+ }),
+ [
+ seek,
+ presentFullscreenPlayer,
+ dismissFullscreenPlayer,
+ save,
+ pause,
+ resume,
+ restoreUserInterfaceForPictureInPictureStopCompleted,
+ ]
+ );
+
+ return (
+
+
+ {showPoster ? (
+
+ ) : null}
+
+ );
+ }
+);
+
+Video.displayName = "Video";
+export default Video;
\ No newline at end of file
diff --git a/src/VideoNativeComponent.ts b/src/VideoNativeComponent.ts
new file mode 100644
index 00000000..2cf6bbb6
--- /dev/null
+++ b/src/VideoNativeComponent.ts
@@ -0,0 +1,323 @@
+import type { HostComponent, NativeSyntheticEvent, ViewProps } from 'react-native';
+import { NativeModules, requireNativeComponent } from 'react-native';
+import { getViewManagerConfig } from './utils';
+
+// -------- There are types for native component (future codegen) --------
+// if you are looking for types for react component, see src/types/video.ts
+
+type Headers = Record
+
+type VideoSrc = Readonly<{
+ uri?: string;
+ isNetwork?: boolean;
+ isAsset?: boolean;
+ shouldCache?: boolean;
+ type?: string;
+ mainVer?: number;
+ patchVer?: number;
+ requestHeaders?: Headers;
+ startTime?: number;
+ endTime?: number;
+}>
+
+
+export type Filter = 'None' |
+'CIColorInvert' |
+'CIColorMonochrome' |
+'CIColorPosterize' |
+'CIFalseColor' |
+'CIMaximumComponent' |
+'CIMinimumComponent' |
+'CIPhotoEffectChrome' |
+'CIPhotoEffectFade' |
+'CIPhotoEffectInstant' |
+'CIPhotoEffectMono' |
+'CIPhotoEffectNoir' |
+'CIPhotoEffectProcess' |
+'CIPhotoEffectTonal' |
+'CIPhotoEffectTransfer' |
+'CISepiaTone';
+
+export type DrmType = 'widevine' | 'playready' | 'clearkey' | 'fairplay';
+
+type Drm = Readonly<{
+ drmType?: DrmType;
+ licenseServer?: string;
+ headers?: Headers;
+ contentId?: string; // ios
+ certificateUrl?: string; // ios
+ base64Certificate?: boolean; // ios default: false
+ useExternalGetLicense?: boolean; // ios
+}>
+
+type TextTracks = ReadonlyArray>
+
+type TextTrackType = 'system' | 'disabled' | 'title' | 'language' | 'index';
+
+type SelectedTextTrack = Readonly<{
+ selectedTextType: TextTrackType;
+ value?: string;
+ index?: number;
+}>
+
+type AudioTrackType = 'system' | 'disabled' | 'title' | 'language' | 'index';
+
+type SelectedAudioTrack = Readonly<{
+ selectedAudioType: AudioTrackType;
+ value?: string;
+ index?: number;
+}>
+
+export type Seek = Readonly<{
+ time: number;
+ tolerance?: number;
+}>
+
+type BufferConfig = Readonly<{
+ minBufferMs?: number;
+ maxBufferMs?: number;
+ bufferForPlaybackMs?: number;
+ bufferForPlaybackAfterRebufferMs?: number;
+ maxHeapAllocationPercent?: number;
+ minBackBufferMemoryReservePercent?: number;
+ minBufferMemoryReservePercent?: number;
+}>
+
+type SelectedVideoTrack = Readonly<{
+ type: 'auto' | 'disabled' | 'resolution' | 'index';
+ value?: number;
+}>
+
+type SubtitleStyle = Readonly<{
+ fontSize?: number;
+ paddingTop?: number;
+ paddingBottom?: number;
+ paddingLeft?: number;
+ paddingRight?: number;
+}>
+
+export type OnLoadData = Readonly<{
+ currentTime: number;
+ duration: number;
+ naturalSize: Readonly<{
+ width: number;
+ height: number;
+ orientation: 'portrait' | 'landscape';
+ }>;
+}>
+
+export type OnLoadStartData = Readonly<{
+ isNetwork: boolean;
+ type: string;
+ uri: string;
+}>
+
+export type OnBufferData = Readonly<{ isBuffering: boolean }>;
+
+
+export type OnProgressData = Readonly<{
+ currentTime: number;
+ playableDuration: number;
+ seekableDuration: number;
+}>
+
+export type OnBandwidthUpdateData = Readonly<{
+ bitrate: number;
+}>;
+
+export type OnSeekData = Readonly<{
+ currentTime: number;
+ seekTime: number;
+ finished: boolean;
+}>
+
+export type OnPlaybackStateChangedData = Readonly<{
+ isPlaying: boolean;
+}>
+
+export type OnTimedMetadataData = Readonly<{
+ metadata: ReadonlyArray>
+}>
+
+
+export type OnAudioTracksData = Readonly<{
+ audioTracks: ReadonlyArray>
+}>
+
+export type OnTextTracksData = Readonly<{
+ textTracks: ReadonlyArray>
+}>
+
+export type OnVideoTracksData = Readonly<{
+ videoTracks: ReadonlyArray>
+}>
+
+export type OnPlaybackData = Readonly<{
+ playbackRate: number;
+}>;
+
+export type OnExternalPlaybackChangeData = Readonly<{
+ isExternalPlaybackActive: boolean;
+}>
+
+export type OnGetLicenseData = Readonly<{
+ licenseUrl: string;
+ contentId: string;
+ spcBase64: string;
+}>
+
+export type OnPictureInPictureStatusChangedData = Readonly<{
+ isActive: boolean;
+}>
+
+export type OnReceiveAdEventData = Readonly<{
+ event: string;
+}>
+
+export type OnVideoErrorData = Readonly<{
+ error: string;
+}>
+
+export type OnAudioFocusChangedData = Readonly<{
+ hasFocus: boolean;
+}>
+
+export type NativeVideoResizeMode = 'ScaleNone' | 'ScaleToFill' | 'ScaleAspectFit' | 'ScaleAspectFill';
+export interface VideoNativeProps extends ViewProps {
+ src?: VideoSrc;
+ drm?: Drm;
+ adTagUrl?: string;
+ allowsExternalPlayback?: boolean; // ios, true
+ maxBitRate?: number;
+ resizeMode?: NativeVideoResizeMode;
+ repeat?: boolean;
+ automaticallyWaitsToMinimizeStalling?: boolean
+ textTracks?: TextTracks;
+ selectedTextTrack?: SelectedTextTrack;
+ selectedAudioTrack?: SelectedAudioTrack;
+ paused?: boolean;
+ muted?: boolean;
+ controls?: boolean;
+ filter?: Filter;
+ filterEnabled?: boolean;
+ volume?: number; // default 1.0
+ playInBackground?: boolean;
+ preventsDisplaySleepDuringVideoPlayback?: boolean;
+ preferredForwardBufferDuration?: number; //ios, 0
+ playWhenInactive?: boolean; // ios, false
+ pictureInPicture?: boolean; // ios, false
+ ignoreSilentSwitch?: 'inherit' | 'ignore' | 'obey'; // ios, 'inherit'
+ mixWithOthers?: 'inherit' | 'mix' | 'duck'; // ios, 'inherit'
+ rate?: number;
+ fullscreen?: boolean; // ios, false
+ fullscreenAutorotate?: boolean;
+ fullscreenOrientation?: 'all' | 'landscape' | 'portrait';
+ progressUpdateInterval?: number;
+ restoreUserInterfaceForPIPStopCompletionHandler?: boolean;
+ localSourceEncryptionKeyScheme?: string;
+
+ backBufferDurationMs?: number; // Android
+ bufferConfig?: BufferConfig; // Android
+ contentStartTime?: number; // Android
+ currentPlaybackTime?: number; // Android
+ disableDisconnectError?: boolean; // Android
+ focusable?: boolean; // Android
+ hideShutterView?: boolean; // Android
+ minLoadRetryCount?: number; // Android
+ reportBandwidth?: boolean; //Android
+ selectedVideoTrack?: SelectedVideoTrack; // android
+ subtitleStyle?: SubtitleStyle // android
+ trackId?: string; // Android
+ useTextureView?: boolean; // Android
+ useSecureView?: boolean; // Android
+
+ onVideoLoad?: (event: NativeSyntheticEvent) => void;
+ onVideoLoadStart?: (event: NativeSyntheticEvent) => void;
+ onVideoBuffer?: (event: NativeSyntheticEvent) => void;
+ onVideoError?: (event: NativeSyntheticEvent) => void;
+ onVideoProgress?: (event: NativeSyntheticEvent) => void;
+ onBandwidthUpdate?: (event: NativeSyntheticEvent) => void;
+ onVideoSeek?: (event: NativeSyntheticEvent) => void;
+ onVideoEnd?: (event: NativeSyntheticEvent>) => void; // all
+ onVideoAudioBecomingNoisy?: (event: NativeSyntheticEvent>) => void;
+ onVideoFullscreenPlayerWillPresent?: (event: NativeSyntheticEvent>) => void; // ios, android
+ onVideoFullscreenPlayerDidPresent?: (event: NativeSyntheticEvent>) => void; // ios, android
+ onVideoFullscreenPlayerWillDismiss?: (event: NativeSyntheticEvent>) => void; // ios, android
+ onVideoFullscreenPlayerDidDismiss?: (event: NativeSyntheticEvent>) => void; // ios, android
+ onReadyForDisplay?: (event: NativeSyntheticEvent>) => void;
+ onPlaybackRateChange?: (event: NativeSyntheticEvent) => void; // all
+ onVideoExternalPlaybackChange?: (event: NativeSyntheticEvent) => void;
+ onGetLicense?: (event: NativeSyntheticEvent) => void;
+ onPictureInPictureStatusChanged?: (event: NativeSyntheticEvent) => void;
+ onRestoreUserInterfaceForPictureInPictureStop?: (event: NativeSyntheticEvent>) => void;
+ onReceiveAdEvent?: (event: NativeSyntheticEvent) => void;
+ onVideoPlaybackStateChanged?: (event: NativeSyntheticEvent) => void; // android only
+ onVideoIdle?: (event: NativeSyntheticEvent<{}>) => void; // android only (nowhere in document, so do not use as props. just type declaration)
+ onAudioFocusChanged?: (event: NativeSyntheticEvent) => void; // android only (nowhere in document, so do not use as props. just type declaration)
+ onTimedMetadata?: (event: NativeSyntheticEvent) => void; // ios, android
+ onAudioTracks?: (event: NativeSyntheticEvent) => void; // android
+ onTextTracks?: (event: NativeSyntheticEvent) => void; // android
+ onVideoTracks?: (event: NativeSyntheticEvent) => void; // android
+}
+
+export type VideoComponentType = HostComponent;
+
+export interface VideoManagerType {
+ save: (reactTag: number) => Promise;
+ setPlayerPauseState: (paused: boolean, reactTag: number) => Promise;
+ setLicenseResult: (result: string, licenseUrl: string, reactTag: number) => Promise;
+ setLicenseResultError: (error: string, licenseUrl: string, reactTag: number) => Promise;
+}
+
+export interface VideoDecoderPropertiesType {
+ getWidevineLevel: () => Promise;
+ isCodecSupported: (mimeType: string, width: number, height: number) => Promise<'unsupported' | 'hardware' | 'software'>;
+ isHEVCSupported: () => Promise<'unsupported' | 'hardware' | 'software'>;
+}
+
+export type VideoViewManagerConfig = {
+ Constants: {
+ ScaleNone: any;
+ ScaleToFill: any;
+ ScaleAspectFit: any;
+ ScaleAspectFill: any;
+ };
+ Commands: { [key: string]: number; };
+};
+
+export const VideoManager = NativeModules.VideoManager as VideoManagerType;
+export const VideoDecoderProperties = NativeModules.VideoDecoderProperties as VideoDecoderPropertiesType;
+export const RCTVideoConstants = (getViewManagerConfig('RCTVideo') as VideoViewManagerConfig).Constants;
+
+export default requireNativeComponent('RCTVideo') as VideoComponentType;
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 00000000..7412ec80
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,11 @@
+import Video from "./Video";
+
+export { default as FilterType } from './lib/FilterType';
+export { default as VideoResizeMode } from './lib/VideoResizeMode';
+export { default as TextTrackType } from './lib/TextTrackType';
+export { default as DRMType } from './lib/DRMType';
+export { VideoDecoderProperties } from './VideoNativeComponent';
+
+export type { VideoRef } from './Video';
+
+export default Video;
\ No newline at end of file
diff --git a/DRMType.js b/src/lib/DRMType.ts
similarity index 91%
rename from DRMType.js
rename to src/lib/DRMType.ts
index d873bd69..e59791fe 100644
--- a/DRMType.js
+++ b/src/lib/DRMType.ts
@@ -3,4 +3,4 @@ export default {
PLAYREADY: 'playready',
CLEARKEY: 'clearkey',
FAIRPLAY: 'fairplay',
-};
+} as const;
diff --git a/FilterType.js b/src/lib/FilterType.ts
similarity index 97%
rename from FilterType.js
rename to src/lib/FilterType.ts
index 3d8d9cb4..e096415f 100644
--- a/FilterType.js
+++ b/src/lib/FilterType.ts
@@ -15,4 +15,4 @@ export default {
TONAL: 'CIPhotoEffectTonal',
TRANSFER: 'CIPhotoEffectTransfer',
SEPIA: 'CISepiaTone',
-};
+} as const;
diff --git a/TextTrackType.js b/src/lib/TextTrackType.ts
similarity index 89%
rename from TextTrackType.js
rename to src/lib/TextTrackType.ts
index 9f06e5dd..c7a0dabd 100644
--- a/TextTrackType.js
+++ b/src/lib/TextTrackType.ts
@@ -2,4 +2,4 @@ export default {
SRT: 'application/x-subrip',
TTML: 'application/ttml+xml',
VTT: 'text/vtt',
-};
+} as const;
diff --git a/src/lib/VideoResizeMode.ts b/src/lib/VideoResizeMode.ts
new file mode 100644
index 00000000..235803e2
--- /dev/null
+++ b/src/lib/VideoResizeMode.ts
@@ -0,0 +1,5 @@
+export default {
+ contain: 'contain',
+ cover: 'cover',
+ stretch: 'stretch',
+} as const;
\ No newline at end of file
diff --git a/src/types/events.ts b/src/types/events.ts
new file mode 100644
index 00000000..cbdacc9e
--- /dev/null
+++ b/src/types/events.ts
@@ -0,0 +1,30 @@
+import type { OnBandwidthUpdateData, OnBufferData, OnLoadData, OnLoadStartData, OnProgressData, OnSeekData, OnPlaybackData, OnExternalPlaybackChangeData, OnPictureInPictureStatusChangedData, OnReceiveAdEventData, OnVideoErrorData, OnPlaybackStateChangedData, OnAudioFocusChangedData, OnTimedMetadataData, OnAudioTracksData, OnTextTracksData, OnVideoTracksData } from "../VideoNativeComponent";
+
+export interface ReactVideoEvents {
+ onAudioBecomingNoisy?: () => void //Android, iOS
+ onAudioFocusChanged?: (e: OnAudioFocusChangedData) => void // Android
+ onIdle?: () => void // Android
+ onBandwidthUpdate?: (e: OnBandwidthUpdateData) => void //Android
+ onBuffer?: (e: OnBufferData) => void //Android, iOS
+ onEnd?: () => void //All
+ onError?: (e: OnVideoErrorData) => void //Android, iOS
+ onExternalPlaybackChange?: (e: OnExternalPlaybackChangeData) => void //iOS
+ onFullscreenPlayerWillPresent?: () => void //Android, iOS
+ onFullscreenPlayerDidPresent?: () => void //Android, iOS
+ onFullscreenPlayerWillDismiss?: () => void //Android, iOS
+ onFullscreenPlayerDidDismiss?: () => void //Android, iOS
+ onLoad?: (e: OnLoadData) => void //All
+ onLoadStart?: (e: OnLoadStartData) => void //All
+ onPictureInPictureStatusChanged?: (e: OnPictureInPictureStatusChangedData) => void //iOS
+ onPlaybackRateChange?: (e: OnPlaybackData) => void //All
+ onProgress?: (e: OnProgressData) => void //All
+ onReadyForDisplay?: () => void //Android, iOS, Web
+ onReceiveAdEvent?: (e: OnReceiveAdEventData) => void //Android, iOS
+ onRestoreUserInterfaceForPictureInPictureStop?: () => void //iOS
+ onSeek?: (e: OnSeekData) => void //Android, iOS, Windows UWP
+ onPlaybackStateChanged?: (e: OnPlaybackStateChangedData) => void // Android
+ onTimedMetadata?: (e: OnTimedMetadataData) => void //Android, iOS
+ onAudioTracks?: (e: OnAudioTracksData) => void // Android
+ onTextTracks?: (e: OnTextTracksData) => void //Android
+ onVideoTracks?: (e: OnVideoTracksData) => void //Android
+}
\ No newline at end of file
diff --git a/src/types/language.ts b/src/types/language.ts
new file mode 100644
index 00000000..ec721833
--- /dev/null
+++ b/src/types/language.ts
@@ -0,0 +1,184 @@
+export type ISO639_1 =
+ | 'aa'
+ | 'ab'
+ | 'ae'
+ | 'af'
+ | 'ak'
+ | 'am'
+ | 'an'
+ | 'ar'
+ | 'as'
+ | 'av'
+ | 'ay'
+ | 'az'
+ | 'ba'
+ | 'be'
+ | 'bg'
+ | 'bi'
+ | 'bm'
+ | 'bn'
+ | 'bo'
+ | 'br'
+ | 'bs'
+ | 'ca'
+ | 'ce'
+ | 'ch'
+ | 'co'
+ | 'cr'
+ | 'cs'
+ | 'cu'
+ | 'cv'
+ | 'cy'
+ | 'da'
+ | 'de'
+ | 'dv'
+ | 'dz'
+ | 'ee'
+ | 'el'
+ | 'en'
+ | 'eo'
+ | 'es'
+ | 'et'
+ | 'eu'
+ | 'fa'
+ | 'ff'
+ | 'fi'
+ | 'fj'
+ | 'fo'
+ | 'fr'
+ | 'fy'
+ | 'ga'
+ | 'gd'
+ | 'gl'
+ | 'gn'
+ | 'gu'
+ | 'gv'
+ | 'ha'
+ | 'he'
+ | 'hi'
+ | 'ho'
+ | 'hr'
+ | 'ht'
+ | 'hu'
+ | 'hy'
+ | 'hz'
+ | 'ia'
+ | 'id'
+ | 'ie'
+ | 'ig'
+ | 'ii'
+ | 'ik'
+ | 'io'
+ | 'is'
+ | 'it'
+ | 'iu'
+ | 'ja'
+ | 'jv'
+ | 'ka'
+ | 'kg'
+ | 'ki'
+ | 'kj'
+ | 'kk'
+ | 'kl'
+ | 'km'
+ | 'kn'
+ | 'ko'
+ | 'kr'
+ | 'ks'
+ | 'ku'
+ | 'kv'
+ | 'kw'
+ | 'ky'
+ | 'la'
+ | 'lb'
+ | 'lg'
+ | 'li'
+ | 'ln'
+ | 'lo'
+ | 'lt'
+ | 'lu'
+ | 'lv'
+ | 'mg'
+ | 'mh'
+ | 'mi'
+ | 'mk'
+ | 'ml'
+ | 'mn'
+ | 'mr'
+ | 'ms'
+ | 'mt'
+ | 'my'
+ | 'na'
+ | 'nb'
+ | 'nd'
+ | 'ne'
+ | 'ng'
+ | 'nl'
+ | 'nn'
+ | 'no'
+ | 'nr'
+ | 'nv'
+ | 'ny'
+ | 'oc'
+ | 'oj'
+ | 'om'
+ | 'or'
+ | 'os'
+ | 'pa'
+ | 'pi'
+ | 'pl'
+ | 'ps'
+ | 'pt'
+ | 'qu'
+ | 'rm'
+ | 'rn'
+ | 'ro'
+ | 'ru'
+ | 'rw'
+ | 'sa'
+ | 'sc'
+ | 'sd'
+ | 'se'
+ | 'sg'
+ | 'si'
+ | 'sk'
+ | 'sl'
+ | 'sm'
+ | 'sn'
+ | 'so'
+ | 'sq'
+ | 'sr'
+ | 'ss'
+ | 'st'
+ | 'su'
+ | 'sv'
+ | 'sw'
+ | 'ta'
+ | 'te'
+ | 'tg'
+ | 'th'
+ | 'ti'
+ | 'tk'
+ | 'tl'
+ | 'tn'
+ | 'to'
+ | 'tr'
+ | 'ts'
+ | 'tt'
+ | 'tw'
+ | 'ty'
+ | 'ug'
+ | 'uk'
+ | 'ur'
+ | 'uz'
+ | 've'
+ | 'vi'
+ | 'vo'
+ | 'wa'
+ | 'wo'
+ | 'xh'
+ | 'yi'
+ | 'yo'
+ | 'za'
+ | 'zh'
+ | 'zu';
\ No newline at end of file
diff --git a/src/types/video.ts b/src/types/video.ts
new file mode 100644
index 00000000..2a824cc3
--- /dev/null
+++ b/src/types/video.ts
@@ -0,0 +1,135 @@
+import type { ISO639_1 } from './language';
+import type { ReactVideoEvents } from './events';
+import type { StyleProp, ViewStyle } from 'react-native'
+
+type Filter = | 'None'
+ | 'CIColorInvert'
+ | 'CIColorMonochrome'
+ | 'CIColorPosterize'
+ | 'CIFalseColor'
+ | 'CIMaximumComponent'
+ | 'CIMinimumComponent'
+ | 'CIPhotoEffectChrome'
+ | 'CIPhotoEffectFade'
+ | 'CIPhotoEffectInstant'
+ | 'CIPhotoEffectMono'
+ | 'CIPhotoEffectNoir'
+ | 'CIPhotoEffectProcess'
+ | 'CIPhotoEffectTonal'
+ | 'CIPhotoEffectTransfer'
+ | 'CISepiaTone'
+
+
+
+type Headers = Record;
+
+export type ReactVideoSource = Readonly<{
+ uri?: string;
+ isNetwork?: boolean;
+ isAsset?: boolean;
+ shouldCache?: boolean;
+ type?: string;
+ mainVer?: number;
+ patchVer?: number;
+ headers?: Headers;
+ startTime?: number;
+ endTime?: number;
+}>;
+
+export type ReactVideoDrm = Readonly<{
+ type?: 'widevine' | 'playready' | 'clearkey' | 'fairplay';
+ licenseServer?: string;
+ headers?: Headers;
+ contentId?: string; // ios
+ certificateUrl?: string; // ios
+ base64Certificate?: boolean; // ios default: false
+ getLicense?: (licenseUrl: string, contentId: string, spcBase64: string) => void; // ios
+}>
+
+type BufferConfig = {
+ minBufferMs?: number;
+ maxBufferMs?: number;
+ bufferForPlaybackMs?: number;
+ bufferForPlaybackAfterRebufferMs?: number;
+ maxHeapAllocationPercent?: number;
+ minBackBufferMemoryReservePercent?: number;
+ minBufferMemoryReservePercent?: number;
+}
+
+type SelectedTrack = {
+ type: 'system' | 'disabled' | 'title' | 'language' | 'index';
+ value?: string | number;
+}
+
+type SelectedVideoTrack = {
+ type: 'auto' | 'disabled' | 'resolution' | 'index'
+ value?: number;
+}
+
+type SubtitleStyle = {
+ fontSize?: number;
+ paddingTop?: number;
+ paddingBottom?: number;
+ paddingLeft?: number;
+ paddingRight?: number;
+}
+
+type TextTracks = {
+ title: string;
+ language: ISO639_1;
+ type: | 'application/x-subrip'
+ | 'application/ttml+xml'
+ | 'text/vtt';
+ uri: string;
+}[]
+
+export interface ReactVideoProps extends ReactVideoEvents {
+ source?: ReactVideoSource;
+ drm?: ReactVideoDrm;
+ style?: StyleProp;
+ adTagUrl?: string; // iOS
+ audioOnly?: boolean;
+ automaticallyWaitsToMinimizeStalling?: boolean; // iOS
+ backBufferDurationMs?: number; // Android
+ bufferConfig?: BufferConfig; // Android
+ contentStartTime?: number; // Android
+ controls?: boolean;
+ currentPlaybackTime?: number; // Android
+ disableFocus?: boolean;
+ disableDisconnectError?: boolean; // Android
+ filter?: Filter; // iOS
+ filterEnabled?: boolean; // iOS
+ focusable?: boolean; // Android
+ fullscreen?: boolean; // iOS
+ fullscreenAutorotate?: boolean; // iOS
+ fullscreenOrientation?: 'all' | 'landscape' | 'portrait'; // iOS
+ hideShutterView?: boolean; // Android
+ ignoreSilentSwitch?: 'inherit' | 'ignore' | 'obey' // iOS
+ minLoadRetryCount?: number; // Android
+ maxBitRate?: number;
+ mixWithOthers?: 'inherit' | 'mix' | 'duck'; // iOS
+ muted?: boolean;
+ paused?: boolean;
+ pictureInPicture?: boolean // iOS
+ playInBackground?: boolean;
+ playWhenInactive?: boolean // iOS
+ poster?: string;
+ posterResizeMode?: 'contain' | 'center' | 'cover' | 'none' | 'repeat' | 'stretch';
+ preferredForwardBufferDuration?: number// iOS
+ preventsDisplaySleepDuringVideoPlayback?: boolean;
+ progressUpdateInterval?: number;
+ rate?: number;
+ repeat?: boolean;
+ reportBandwidth?: boolean; //Android
+ resizeMode?: 'none' | 'contain' | 'cover' | 'stretch';
+ selectedAudioTrack?: SelectedTrack;
+ selectedTextTrack?: SelectedTrack;
+ selectedVideoTrack?: SelectedVideoTrack; // android
+ subtitleStyle?: SubtitleStyle // android
+ textTracks?: TextTracks;
+ trackId?: string; // Android
+ useTextureView?: boolean; // Android
+ useSecureView?: boolean; // Android
+ volume?: number;
+ localSourceEncryptionKeyScheme?: string;
+}
\ No newline at end of file
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 00000000..8a32035d
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,37 @@
+import type { Component, RefObject, ComponentClass } from 'react';
+import { Image, UIManager, findNodeHandle } from "react-native";
+import type { ImageSourcePropType } from 'react-native';
+import type { ReactVideoSource } from './types/video';
+
+type Source = ImageSourcePropType | ReactVideoSource;
+
+export function resolveAssetSourceForVideo(source: Source): ReactVideoSource {
+ if (typeof source === 'number') {
+ return {
+ uri: Image.resolveAssetSource(source).uri,
+ };
+ }
+ return source as ReactVideoSource;
+}
+
+export function getReactTag(ref: RefObject | ComponentClass | null>): number {
+ if (!ref.current) {
+ throw new Error("Video Component is not mounted");
+ }
+
+ const reactTag = findNodeHandle(ref.current);
+
+ if (!reactTag) {
+ throw new Error("Cannot find reactTag for Video Component in components tree");
+ }
+
+ return reactTag;
+}
+
+export function getViewManagerConfig(name: string) {
+ if('getViewManagerConfig' in UIManager) {
+ return UIManager.getViewManagerConfig(name);
+ }
+
+ return UIManager[name];
+}
\ No newline at end of file
diff --git a/tsconfig.build.json b/tsconfig.build.json
new file mode 100644
index 00000000..e00766cd
--- /dev/null
+++ b/tsconfig.build.json
@@ -0,0 +1,5 @@
+
+{
+ "extends": "./tsconfig",
+ "exclude": ["examples", "lib"]
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..01118ee5
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "lib",
+ "paths": {
+ "react-native-video": ["./src/index"]
+ },
+ "composite": true,
+ "declaration": true,
+ "sourceMap": true,
+ "allowUnreachableCode": false,
+ "allowUnusedLabels": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "jsx": "react",
+ "lib": ["esnext"],
+ "module": "esnext",
+ "moduleResolution": "node",
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitReturns": true,
+ "noImplicitUseStrict": false,
+ "noStrictGenericChecks": false,
+ "noUncheckedIndexedAccess": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "esnext",
+ "verbatimModuleSyntax": true
+ },
+ "exclude": ["examples", "lib"]
+}