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
20 changed files with 1259 additions and 619 deletions

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;

6
src/lib/DRMType.ts Normal file
View File

@@ -0,0 +1,6 @@
export default {
WIDEVINE: 'widevine',
PLAYREADY: 'playready',
CLEARKEY: 'clearkey',
FAIRPLAY: 'fairplay',
} as const;

18
src/lib/FilterType.ts Normal file
View File

@@ -0,0 +1,18 @@
export default {
NONE: '',
INVERT: 'CIColorInvert',
MONOCHROME: 'CIColorMonochrome',
POSTERIZE: 'CIColorPosterize',
FALSE: 'CIFalseColor',
MAXIMUMCOMPONENT: 'CIMaximumComponent',
MINIMUMCOMPONENT: 'CIMinimumComponent',
CHROME: 'CIPhotoEffectChrome',
FADE: 'CIPhotoEffectFade',
INSTANT: 'CIPhotoEffectInstant',
MONO: 'CIPhotoEffectMono',
NOIR: 'CIPhotoEffectNoir',
PROCESS: 'CIPhotoEffectProcess',
TONAL: 'CIPhotoEffectTonal',
TRANSFER: 'CIPhotoEffectTransfer',
SEPIA: 'CISepiaTone',
} as const;

5
src/lib/TextTrackType.ts Normal file
View File

@@ -0,0 +1,5 @@
export default {
SRT: 'application/x-subrip',
TTML: 'application/ttml+xml',
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];
}