feat: add typescript (#3266)

* chore: update dependencies
* chore: add typescript config
* feat: add types
* chore: add build command
* chore: fix types
* fix: update linters
* chore: add display name to component
* chore: fix types
* chore: remove re-declare name variables
* docs: update changelog
This commit is contained in:
Krzysztof Moch 2023-10-06 18:39:14 +02:00 committed by GitHub
parent f4acaccd80
commit 92831afd5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1259 additions and 619 deletions

View File

@ -1,3 +1,10 @@
{ {
"extends": "@react-native-community", "extends": [
"@react-native",
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"requireConfigFile": false
}
} }

7
.gitignore vendored
View File

@ -54,4 +54,9 @@ android/keystores/debug.keystore
# windows # windows
Deploy.binlog Deploy.binlog
msbuild.binlog msbuild.binlog
android/buildOutput_* android/buildOutput_*
# lib build
lib/
!src/lib
*.tsbuildinfo

View File

@ -1,8 +1,6 @@
{ {
"requirePragma": true,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all", "trailingComma": "all",
"bracketSpacing": false, "bracketSpacing": false,
"jsxBracketSameLine": true, "jsxBracketSameLine": true
"parser": "flow"
} }

View File

@ -1,6 +1,7 @@
## Changelog ## Changelog
## Next ## 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) - 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 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) - Windows: fix `onError` not being raised [#3247](https://github.com/react-native-video/react-native-video/pull/3247)

587
Video.js
View File

@ -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 (
<View style={nativeProps.style}>
<RCTVideo
ref={this._assignRoot}
{...nativeProps}
style={StyleSheet.absoluteFill}
/>
{this.state.showPoster && (
<Image style={posterStyle} source={{ uri: this.props.poster }} />
)}
</View>
);
}
}
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');

View File

@ -1,7 +0,0 @@
import keyMirror from 'keymirror';
export default keyMirror({
contain: null,
cover: null,
stretch: null,
});

View File

@ -2,8 +2,9 @@
"name": "react-native-video", "name": "react-native-video",
"version": "6.0.0-alpha.8", "version": "6.0.0-alpha.8",
"description": "A <Video /> element for react-native", "description": "A <Video /> element for react-native",
"main": "Video.js", "main": "lib/index",
"source": "Video.js", "source": "src/index",
"react-native": "src/index",
"license": "MIT", "license": "MIT",
"author": "Community Contributors", "author": "Community Contributors",
"homepage": "https://github.com/react-native-video/react-native-video#readme", "homepage": "https://github.com/react-native-video/react-native-video#readme",
@ -11,24 +12,32 @@
"type": "git", "type": "git",
"url": "git@github.com:react-native-video/react-native-video.git" "url": "git@github.com:react-native-video/react-native-video.git"
}, },
"resolutions": {
"@types/react": "~18.0.0"
},
"devDependencies": { "devDependencies": {
"@react-native-community/eslint-config": "^0.0.5", "@react-native/eslint-config": "^0.72.2",
"eslint": "^6.5.1", "@types/jest": "^28.1.2",
"react": "16.9.0", "@types/react": "~18.0.0",
"react-native": "0.61.5", "@types/react-native": "0.72.3",
"react-native-windows": "^0.61.0-0" "@typescript-eslint/eslint-plugin": "^6.7.4",
}, "eslint": "^8.19.0",
"dependencies": { "eslint-plugin-jest": "^27.4.2",
"deprecated-react-native-prop-types": "^2.2.0", "jest": "^29.7.0",
"keymirror": "^0.1.1", "prettier": "^2.4.1",
"prop-types": "^15.7.2" "react": "18.2.0",
"react-native": "0.72.5",
"react-native-windows": "^0.61.0-0",
"typescript": "5.1.6"
}, },
"dependencies": {},
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",
"react-native": "*" "react-native": "*"
}, },
"scripts": { "scripts": {
"lint": "yarn eslint .", "lint": "yarn eslint .",
"build": "yarn tsc",
"xbasic": "yarn --cwd examples/basic", "xbasic": "yarn --cwd examples/basic",
"test": "echo no test available" "test": "echo no test available"
}, },
@ -36,11 +45,13 @@
"android", "android",
"ios", "ios",
"windows", "windows",
"FilterType.js", "src",
"DRMType.js",
"TextTrackType.js",
"VideoResizeMode.js",
"react-native-video.podspec", "react-native-video.podspec",
"docs" "docs",
"!android/build",
"!android/buildOutput_*",
"!android/local.properties",
"!ios/build",
"!**/*.tsbuildinfo"
] ]
} }

449
src/Video.tsx Normal file
View File

@ -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<VideoRef, ReactVideoProps>(
(
{
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<ComponentRef<VideoComponentType>>(null);
const [showPoster, setShowPoster] = useState(!!poster);
const [isFullscreen, setIsFullscreen] = useState(fullscreen);
const [
_restoreUserInterfaceForPIPStopCompletionHandler,
setRestoreUserInterfaceForPIPStopCompletionHandler,
] = useState<boolean | undefined>();
const posterStyle = useMemo<StyleProp<ImageStyle>>(
() => ({
...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<OnLoadStartData>) => {
onLoadStart?.(e.nativeEvent);
},
[onLoadStart]
);
const onVideoLoad = useCallback(
(e: NativeSyntheticEvent<OnLoadData>) => {
if (Platform.OS === "windows") setShowPoster(false);
onLoad?.(e.nativeEvent);
},
[onLoad, setShowPoster]
);
const onVideoError = useCallback(
(e: NativeSyntheticEvent<OnVideoErrorData>) => {
onError?.(e.nativeEvent);
},
[onError]
);
const onVideoProgress = useCallback(
(e: NativeSyntheticEvent<OnProgressData>) => {
onProgress?.(e.nativeEvent);
},
[onProgress]
);
const onVideoSeek = useCallback(
(e: NativeSyntheticEvent<OnSeekData>) => {
onSeek?.(e.nativeEvent);
},
[onSeek]
);
// android only
const onVideoPlaybackStateChanged = useCallback((e: NativeSyntheticEvent<OnPlaybackStateChangedData>) => {
onPlaybackStateChanged?.(e.nativeEvent);
}, [onPlaybackStateChanged])
// android only
const onVideoIdle = useCallback(() => {
onIdle?.()
}, [onIdle])
const _onTimedMetadata = useCallback(
(e: NativeSyntheticEvent<OnTimedMetadataData>) => {
onTimedMetadata?.(e.nativeEvent);
},
[onTimedMetadata]
);
const _onAudioTracks = useCallback((e: NativeSyntheticEvent<OnAudioTracksData>) => {
onAudioTracks?.(e.nativeEvent)
}, [onAudioTracks])
const _onTextTracks = useCallback((e: NativeSyntheticEvent<OnTextTracksData>) => {
onTextTracks?.(e.nativeEvent)
}, [onTextTracks])
const _onVideoTracks = useCallback((e: NativeSyntheticEvent<OnVideoTracksData>) => {
onVideoTracks?.(e.nativeEvent)
}, [onVideoTracks])
const _onPlaybackRateChange = useCallback(
(e: NativeSyntheticEvent<Readonly<{ playbackRate: number }>>) => {
onPlaybackRateChange?.(e.nativeEvent);
},
[onPlaybackRateChange]
);
const _onReadyForDisplay = useCallback(() => {
setShowPoster(false);
onReadyForDisplay?.();
}, [setShowPoster, onReadyForDisplay]);
const _onPictureInPictureStatusChanged = useCallback(
(e: NativeSyntheticEvent<OnPictureInPictureStatusChangedData>) => {
onPictureInPictureStatusChanged?.(e.nativeEvent);
},
[onPictureInPictureStatusChanged]
);
const _onAudioFocusChanged = useCallback((e: NativeSyntheticEvent<OnAudioFocusChangedData>) => {
onAudioFocusChanged?.(e.nativeEvent)
}, [onAudioFocusChanged])
const onVideoBuffer = useCallback((e: NativeSyntheticEvent<OnBufferData>) => {
onBuffer?.(e.nativeEvent);
}, [onBuffer]);
const onVideoExternalPlaybackChange = useCallback((e: NativeSyntheticEvent<OnExternalPlaybackChangeData>) => {
onExternalPlaybackChange?.(e.nativeEvent);
}, [onExternalPlaybackChange])
const _onBandwidthUpdate = useCallback((e: NativeSyntheticEvent<OnBandwidthUpdateData>) => {
onBandwidthUpdate?.(e.nativeEvent);
}, [onBandwidthUpdate]);
const _onReceiveAdEvent = useCallback((e: NativeSyntheticEvent<OnReceiveAdEventData>) => {
onReceiveAdEvent?.(e.nativeEvent);
}, [onReceiveAdEvent]);
const onGetLicense = useCallback(
(event: NativeSyntheticEvent<OnGetLicenseData>) => {
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 (
<View style={style}>
<NativeVideoComponent
ref={nativeRef}
{...rest}
src={src}
drm={_drm}
style={StyleSheet.absoluteFill}
resizeMode={_resizeMode}
fullscreen={isFullscreen}
restoreUserInterfaceForPIPStopCompletionHandler={
_restoreUserInterfaceForPIPStopCompletionHandler
}
textTracks={textTracks}
selectedTextTrack={_selectedTextTrack}
selectedAudioTrack={_selectedAudioTrack}
onGetLicense={onGetLicense}
onVideoLoad={onVideoLoad}
onVideoLoadStart={onVideoLoadStart}
onVideoError={onVideoError}
onVideoProgress={onVideoProgress}
onVideoSeek={onVideoSeek}
onVideoEnd={onEnd}
onVideoBuffer={onVideoBuffer}
onVideoPlaybackStateChanged={onVideoPlaybackStateChanged}
onBandwidthUpdate={_onBandwidthUpdate}
onTimedMetadata={_onTimedMetadata}
onAudioTracks={_onAudioTracks}
onTextTracks={_onTextTracks}
onVideoTracks={_onVideoTracks}
onVideoFullscreenPlayerDidDismiss={onFullscreenPlayerDidDismiss}
onVideoFullscreenPlayerDidPresent={onFullscreenPlayerDidPresent}
onVideoFullscreenPlayerWillDismiss={onFullscreenPlayerWillDismiss}
onVideoFullscreenPlayerWillPresent={onFullscreenPlayerWillPresent}
onVideoExternalPlaybackChange={onVideoExternalPlaybackChange}
onVideoIdle={onVideoIdle}
onAudioFocusChanged={_onAudioFocusChanged}
onReadyForDisplay={_onReadyForDisplay}
onPlaybackRateChange={_onPlaybackRateChange}
onVideoAudioBecomingNoisy={onAudioBecomingNoisy}
onPictureInPictureStatusChanged={_onPictureInPictureStatusChanged}
onRestoreUserInterfaceForPictureInPictureStop={
onRestoreUserInterfaceForPictureInPictureStop
}
onReceiveAdEvent={_onReceiveAdEvent}
/>
{showPoster ? (
<Image style={posterStyle} source={{ uri: poster }} />
) : null}
</View>
);
}
);
Video.displayName = "Video";
export default Video;

323
src/VideoNativeComponent.ts Normal file
View File

@ -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<string, any>
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<Readonly<{
title: string;
language: string;
type: string;
uri: string;
}>>
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<Readonly<{
value?: string
identifier: string
}>>
}>
export type OnAudioTracksData = Readonly<{
audioTracks: ReadonlyArray<Readonly<{
index: number
title?: string
language?: string
bitrate?: number
type?: string
selected?: boolean
}>>
}>
export type OnTextTracksData = Readonly<{
textTracks: ReadonlyArray<Readonly<{
index: number
title?: string
language?: string
/**
* iOS only supports VTT, Android supports all 3
*/
type?: 'srt' | 'ttml' | 'vtt'
selected?: boolean
}>>
}>
export type OnVideoTracksData = Readonly<{
videoTracks: ReadonlyArray<Readonly<{
trackId: number
codecs?: string
width?: number
height?: number
bitrate?: number
selected?: boolean
}>>
}>
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<OnLoadData>) => void;
onVideoLoadStart?: (event: NativeSyntheticEvent<OnLoadStartData>) => void;
onVideoBuffer?: (event: NativeSyntheticEvent<OnBufferData>) => void;
onVideoError?: (event: NativeSyntheticEvent<OnVideoErrorData>) => void;
onVideoProgress?: (event: NativeSyntheticEvent<OnProgressData>) => void;
onBandwidthUpdate?: (event: NativeSyntheticEvent<OnBandwidthUpdateData>) => void;
onVideoSeek?: (event: NativeSyntheticEvent<OnSeekData>) => void;
onVideoEnd?: (event: NativeSyntheticEvent<Readonly<{}>>) => void; // all
onVideoAudioBecomingNoisy?: (event: NativeSyntheticEvent<Readonly<{}>>) => void;
onVideoFullscreenPlayerWillPresent?: (event: NativeSyntheticEvent<Readonly<{}>>) => void; // ios, android
onVideoFullscreenPlayerDidPresent?: (event: NativeSyntheticEvent<Readonly<{}>>) => void; // ios, android
onVideoFullscreenPlayerWillDismiss?: (event: NativeSyntheticEvent<Readonly<{}>>) => void; // ios, android
onVideoFullscreenPlayerDidDismiss?: (event: NativeSyntheticEvent<Readonly<{}>>) => void; // ios, android
onReadyForDisplay?: (event: NativeSyntheticEvent<Readonly<{}>>) => void;
onPlaybackRateChange?: (event: NativeSyntheticEvent<OnPlaybackData>) => void; // all
onVideoExternalPlaybackChange?: (event: NativeSyntheticEvent<OnExternalPlaybackChangeData>) => void;
onGetLicense?: (event: NativeSyntheticEvent<OnGetLicenseData>) => void;
onPictureInPictureStatusChanged?: (event: NativeSyntheticEvent<OnPictureInPictureStatusChangedData>) => void;
onRestoreUserInterfaceForPictureInPictureStop?: (event: NativeSyntheticEvent<Readonly<{}>>) => void;
onReceiveAdEvent?: (event: NativeSyntheticEvent<OnReceiveAdEventData>) => void;
onVideoPlaybackStateChanged?: (event: NativeSyntheticEvent<OnPlaybackStateChangedData>) => void; // android only
onVideoIdle?: (event: NativeSyntheticEvent<{}>) => void; // android only (nowhere in document, so do not use as props. just type declaration)
onAudioFocusChanged?: (event: NativeSyntheticEvent<OnAudioFocusChangedData>) => void; // android only (nowhere in document, so do not use as props. just type declaration)
onTimedMetadata?: (event: NativeSyntheticEvent<OnTimedMetadataData>) => void; // ios, android
onAudioTracks?: (event: NativeSyntheticEvent<OnAudioTracksData>) => void; // android
onTextTracks?: (event: NativeSyntheticEvent<OnTextTracksData>) => void; // android
onVideoTracks?: (event: NativeSyntheticEvent<OnVideoTracksData>) => void; // android
}
export type VideoComponentType = HostComponent<VideoNativeProps>;
export interface VideoManagerType {
save: (reactTag: number) => Promise<void>;
setPlayerPauseState: (paused: boolean, reactTag: number) => Promise<void>;
setLicenseResult: (result: string, licenseUrl: string, reactTag: number) => Promise<void>;
setLicenseResultError: (error: string, licenseUrl: string, reactTag: number) => Promise<void>;
}
export interface VideoDecoderPropertiesType {
getWidevineLevel: () => Promise<number>;
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<VideoNativeProps>('RCTVideo') as VideoComponentType;

11
src/index.ts Normal file
View File

@ -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;

View File

@ -3,4 +3,4 @@ export default {
PLAYREADY: 'playready', PLAYREADY: 'playready',
CLEARKEY: 'clearkey', CLEARKEY: 'clearkey',
FAIRPLAY: 'fairplay', FAIRPLAY: 'fairplay',
}; } as const;

View File

@ -15,4 +15,4 @@ export default {
TONAL: 'CIPhotoEffectTonal', TONAL: 'CIPhotoEffectTonal',
TRANSFER: 'CIPhotoEffectTransfer', TRANSFER: 'CIPhotoEffectTransfer',
SEPIA: 'CISepiaTone', SEPIA: 'CISepiaTone',
}; } as const;

View File

@ -2,4 +2,4 @@ export default {
SRT: 'application/x-subrip', SRT: 'application/x-subrip',
TTML: 'application/ttml+xml', TTML: 'application/ttml+xml',
VTT: 'text/vtt', VTT: 'text/vtt',
}; } as const;

View File

@ -0,0 +1,5 @@
export default {
contain: 'contain',
cover: 'cover',
stretch: 'stretch',
} as const;

30
src/types/events.ts Normal file
View File

@ -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
}

184
src/types/language.ts Normal file
View File

@ -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';

135
src/types/video.ts Normal file
View File

@ -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<string, string>;
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<ViewStyle>;
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;
}

37
src/utils.ts Normal file
View File

@ -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<Component<any, any, any> | ComponentClass<any, any> | 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];
}

5
tsconfig.build.json Normal file
View File

@ -0,0 +1,5 @@
{
"extends": "./tsconfig",
"exclude": ["examples", "lib"]
}

33
tsconfig.json Normal file
View File

@ -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"]
}