refactor: basic example from class component to functional component (#3934)

* refactor: basic example from class component to functional component

* refactor: toast component path

* refactor: bufferConfig prop

* refacotr: import component path

* fix: seekbar issue on iOS
This commit is contained in:
Seyed Mostafa Hasani 2024-06-28 13:03:10 +03:30 committed by GitHub
parent b431d09e2f
commit d4f1648681
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 802 additions and 723 deletions

View File

@ -1,16 +1,8 @@
'use strict'; 'use strict';
import React, {Component} from 'react'; import React, {FC, useRef, useState} from 'react';
import { import {TouchableOpacity, View} from 'react-native';
Text,
TouchableOpacity,
View,
ActivityIndicator,
ToastAndroid,
Platform,
Alert,
} from 'react-native';
import Video, { import Video, {
AudioTrack, AudioTrack,
@ -20,434 +12,169 @@ import Video, {
OnTextTracksData, OnTextTracksData,
OnVideoAspectRatioData, OnVideoAspectRatioData,
TextTrack, TextTrack,
VideoDecoderProperties,
OnBufferData, OnBufferData,
OnAudioFocusChangedData, OnAudioFocusChangedData,
OnVideoErrorData, OnVideoErrorData,
VideoRef, VideoRef,
ResizeMode,
SelectedTrack,
DRMType,
OnTextTrackDataChangedData, OnTextTrackDataChangedData,
TextTrackType,
ISO639_1,
OnSeekData, OnSeekData,
OnPlaybackStateChangedData, OnPlaybackStateChangedData,
OnPlaybackRateChangeData, OnPlaybackRateChangeData,
OnVideoTracksData, OnVideoTracksData,
VideoTrack,
SelectedVideoTrackType, SelectedVideoTrackType,
SelectedVideoTrack,
BufferingStrategyType, BufferingStrategyType,
ReactVideoSource, ReactVideoSource,
Drm, SelectedTrackType,
TextTracks,
} from 'react-native-video'; } from 'react-native-video';
import ToggleControl from './ToggleControl';
import MultiValueControl, {
MultiValueControlPropType,
} from './MultiValueControl';
import styles from './styles'; import styles from './styles';
import AudioTrackSelector from './components/AudioTracksSelector'; import {AdditionalSourceInfo} from './types';
import TextTrackSelector from './components/TextTracksSelector'; import {
import VideoTrackSelector from './components/VideoTracksSelector'; bufferConfig,
import Seeker from './components/Seeker'; defaultValue,
srcList,
textTracksSelectionBy,
} from './constants';
import {Overlay, toast} from './components';
type AdditionnalSourceInfo = { type Props = NonNullable<unknown>;
textTracks: TextTracks;
adTagUrl: string;
description: string;
drm: Drm;
noView: boolean;
};
type SampleVideoSource = ReactVideoSource | AdditionnalSourceInfo; const VideoPlayer: FC<Props> = ({}) => {
const [state, setState] = useState(defaultValue);
const videoRef = useRef<VideoRef>(null);
const viewStyle = state.fullscreen ? styles.fullScreen : styles.halfScreen;
const currentSrc = srcList[state.srcListId];
const additional = currentSrc as AdditionalSourceInfo;
interface StateType { const onAudioTracks = (data: OnAudioTracksData) => {
rate: number;
volume: number;
muted: boolean;
resizeMode: ResizeMode;
duration: number;
currentTime: number;
videoWidth: number;
videoHeight: number;
paused: boolean;
fullscreen: true;
decoration: true;
isLoading: boolean;
audioTracks: Array<AudioTrack>;
textTracks: Array<TextTrack>;
videoTracks: Array<VideoTrack>;
selectedAudioTrack: SelectedTrack | undefined;
selectedTextTrack: SelectedTrack | undefined;
selectedVideoTrack: SelectedVideoTrack;
srcListId: number;
loop: boolean;
showRNVControls: boolean;
useCache: boolean;
poster?: string;
showNotificationControls: boolean;
isSeeking: boolean;
}
class VideoPlayer extends Component {
state: StateType = {
rate: 1,
volume: 1,
muted: false,
resizeMode: ResizeMode.CONTAIN,
duration: 0.0,
currentTime: 0.0,
videoWidth: 0,
videoHeight: 0,
paused: false,
fullscreen: true,
decoration: true,
isLoading: false,
audioTracks: [],
textTracks: [],
videoTracks: [],
selectedAudioTrack: undefined,
selectedTextTrack: undefined,
selectedVideoTrack: {
type: SelectedVideoTrackType.AUTO,
},
srcListId: 0,
loop: false,
showRNVControls: false,
useCache: false,
poster: undefined,
showNotificationControls: false,
isSeeking: false,
};
// internal usage change to index if you want to select tracks by index instead of lang
textTracksSelectionBy = 'index';
srcAllPlatformList = [
{
description: 'local file landscape',
uri: require('./broadchurch.mp4'),
},
{
description: 'local file landscape cropped',
uri: require('./broadchurch.mp4'),
cropStart: 3000,
cropEnd: 10000,
},
{
description: 'local file portrait',
uri: require('./portrait.mp4'),
metadata: {
title: 'Test Title',
subtitle: 'Test Subtitle',
artist: 'Test Artist',
description: 'Test Description',
imageUri:
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
},
},
{
description: '(hls|live) red bull tv',
textTracksAllowChunklessPreparation: false,
uri: 'https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_928.m3u8',
metadata: {
title: 'Custom Title',
subtitle: 'Custom Subtitle',
artist: 'Custom Artist',
description: 'Custom Description',
imageUri:
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
},
},
{
description: 'invalid URL',
uri: 'mmt://www.youtube.com',
type: 'mpd',
},
{description: '(no url) Stopped playback', uri: undefined},
{
description: '(no view) no View',
noView: true,
},
{
description: 'Another live sample',
uri: 'https://live.forstreet.cl/live/livestream.m3u8',
},
{
description: 'another bunny (can be saved)',
uri: 'https://rawgit.com/mediaelement/mediaelement-files/master/big_buck_bunny.mp4',
headers: {referer: 'www.github.com', 'User-Agent': 'react.native.video'},
},
{
description: 'sintel with subtitles',
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
},
{
description: 'sintel starts at 20sec',
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
startPosition: 50000,
},
{
description: 'BigBugBunny sideLoaded subtitles',
// sideloaded subtitles wont work for streaming like HLS on ios
// mp4
uri: 'https://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
textTracks: [
{
title: 'test',
language: 'en' as ISO639_1,
type: TextTrackType.VTT,
uri: 'https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt',
},
],
},
];
srcIosList = [];
srcAndroidList = [
{
description: 'Another live sample',
uri: 'https://live.forstreet.cl/live/livestream.m3u8',
},
{
description: 'asset file',
uri: 'asset:///broadchurch.mp4',
},
{
description: '(dash) sintel subtitles',
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/sintel.mpd',
},
{
description: '(mp4) big buck bunny',
uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
},
{
description: '(mp4|subtitles) demo with sintel Subtitles',
uri: 'http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0',
type: 'mpd',
},
{
description: '(mp4) big buck bunny With Ads',
adTagUrl:
'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostoptimizedpodbumper&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator=',
uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
},
{
description: 'WV: Secure SD & HD (cbcs,MP4,H264)',
uri: 'https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd',
drm: {
type: DRMType.WIDEVINE,
licenseServer:
'https://proxy.uat.widevine.com/proxy?provider=widevine_test',
},
},
{
description: 'Secure UHD (cenc)',
uri: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd',
drm: {
type: DRMType.WIDEVINE,
licenseServer:
'https://proxy.uat.widevine.com/proxy?provider=widevine_test',
},
},
{
description: 'rtsp big bug bunny',
uri: 'rtsp://rtspstream:3cfa3c36a9c00f4aa38f3cd35816b287@zephyr.rtsp.stream/movie',
type: 'rtsp',
},
];
// poster which can be displayed
samplePoster =
'https://upload.wikimedia.org/wikipedia/commons/1/18/React_Native_Logo.png';
srcList: SampleVideoSource[] = this.srcAllPlatformList.concat(
Platform.OS === 'android' ? this.srcAndroidList : this.srcIosList,
);
video?: VideoRef;
popupInfo = () => {
VideoDecoderProperties.getWidevineLevel().then((widevineLevel: number) => {
VideoDecoderProperties.isHEVCSupported().then((hevc: string) => {
VideoDecoderProperties.isCodecSupported('video/avc', 1920, 1080).then(
(avc: string) => {
this.toast(
true,
'Widevine level: ' +
widevineLevel +
'\n hevc: ' +
hevc +
'\n avc: ' +
avc,
);
},
);
});
});
};
onLoad = (data: OnLoadData) => {
this.setState({duration: data.duration, loading: false});
this.onAudioTracks(data);
this.onTextTracks(data);
this.onVideoTracks(data);
};
onProgress = (data: OnProgressData) => {
this.setState({currentTime: data.currentTime});
};
onSeek = (data: OnSeekData) => {
this.setState({isSeeking: false});
this.setState({currentTime: data.currentTime});
};
onVideoLoadStart = () => {
console.log('onVideoLoadStart');
this.setState({isLoading: true});
};
onAudioTracks = (data: OnAudioTracksData) => {
const selectedTrack = data.audioTracks?.find((x: AudioTrack) => { const selectedTrack = data.audioTracks?.find((x: AudioTrack) => {
return x.selected; return x.selected;
}); });
if (selectedTrack?.index) { if (selectedTrack?.index) {
this.setState({ setState({
...state,
audioTracks: data.audioTracks, audioTracks: data.audioTracks,
selectedAudioTrack: { selectedAudioTrack: {
type: SelectedVideoTrackType.INDEX, type: SelectedTrackType.INDEX,
value: selectedTrack?.index, value: selectedTrack?.index,
}, },
}); });
} else { } else {
this.setState({ setState({
...state,
audioTracks: data.audioTracks, audioTracks: data.audioTracks,
}); });
} }
}; };
onVideoTracks = (data: OnVideoTracksData) => { const onVideoTracks = (data: OnVideoTracksData) => {
console.log('onVideoTracks', data.videoTracks); console.log('onVideoTracks', data.videoTracks);
this.setState({ setState({
...state,
videoTracks: data.videoTracks, videoTracks: data.videoTracks,
}); });
}; };
onTextTracks = (data: OnTextTracksData) => { const onTextTracks = (data: OnTextTracksData) => {
const selectedTrack = data.textTracks?.find((x: TextTrack) => { const selectedTrack = data.textTracks?.find((x: TextTrack) => {
return x?.selected; return x?.selected;
}); });
if (selectedTrack?.language) { if (selectedTrack?.language) {
this.setState({ setState({
...state,
textTracks: data.textTracks, textTracks: data.textTracks,
selectedTextTrack: selectedTextTrack:
this.textTracksSelectionBy === 'index' textTracksSelectionBy === 'index'
? { ? {
type: 'index', type: SelectedTrackType.INDEX,
value: selectedTrack?.index, value: selectedTrack?.index,
} }
: { : {
type: 'language', type: SelectedTrackType.LANGUAGE,
value: selectedTrack?.language, value: selectedTrack?.language,
}, },
}); });
} else { } else {
this.setState({ setState({
...state,
textTracks: data.textTracks, textTracks: data.textTracks,
}); });
} }
}; };
onTextTrackDataChanged = (data: OnTextTrackDataChangedData) => { const onLoad = (data: OnLoadData) => {
onAudioTracks(data);
onTextTracks(data);
onVideoTracks(data);
setState({...state, duration: data.duration});
};
const onProgress = (data: OnProgressData) => {
setState({...state, currentTime: data.currentTime});
};
const onSeek = (data: OnSeekData) => {
setState({...state, currentTime: data.currentTime, isSeeking: false});
};
const onVideoLoadStart = () => {
console.log('onVideoLoadStart');
setState({...state, isLoading: true});
};
const onTextTrackDataChanged = (data: OnTextTrackDataChangedData) => {
console.log(`Subtitles: ${JSON.stringify(data, null, 2)}`); console.log(`Subtitles: ${JSON.stringify(data, null, 2)}`);
}; };
onAspectRatio = (data: OnVideoAspectRatioData) => { const onAspectRatio = (data: OnVideoAspectRatioData) => {
console.log('onAspectRadio called ' + JSON.stringify(data)); console.log('onAspectRadio called ' + JSON.stringify(data));
this.setState({ setState({
...state,
videoWidth: data.width, videoWidth: data.width,
videoHeight: data.height, videoHeight: data.height,
}); });
}; };
onVideoBuffer = (param: OnBufferData) => { const onVideoBuffer = (param: OnBufferData) => {
console.log('onVideoBuffer'); console.log('onVideoBuffer');
this.setState({isLoading: param.isBuffering}); setState({...state, isLoading: param.isBuffering});
}; };
onReadyForDisplay = () => { const onReadyForDisplay = () => {
console.log('onReadyForDisplay'); console.log('onReadyForDisplay');
this.setState({isLoading: false}); setState({...state, isLoading: false});
}; };
onAudioBecomingNoisy = () => { const onAudioBecomingNoisy = () => {
this.setState({paused: true}); setState({...state, paused: true});
}; };
onAudioFocusChanged = (event: OnAudioFocusChangedData) => { const onAudioFocusChanged = (event: OnAudioFocusChangedData) => {
this.setState({paused: !event.hasAudioFocus}); setState({...state, paused: !event.hasAudioFocus});
}; };
toast = (visible: boolean, message: string) => { const onError = (err: OnVideoErrorData) => {
if (visible) {
if (Platform.OS === 'android') {
ToastAndroid.showWithGravityAndOffset(
message,
ToastAndroid.LONG,
ToastAndroid.BOTTOM,
25,
50,
);
} else {
Alert.alert(message, message);
}
}
};
onError = (err: OnVideoErrorData) => {
console.log(JSON.stringify(err)); console.log(JSON.stringify(err));
this.toast(true, 'error: ' + JSON.stringify(err)); toast(true, 'error: ' + JSON.stringify(err));
}; };
onEnd = () => { const onEnd = () => {
if (!this.state.loop) { if (!state.loop) {
this.channelUp(); channelUp();
} }
}; };
onPlaybackRateChange = (data: OnPlaybackRateChangeData) => { const onPlaybackRateChange = (data: OnPlaybackRateChangeData) => {
console.log('onPlaybackRateChange', data); console.log('onPlaybackRateChange', data);
}; };
onPlaybackStateChanged = (data: OnPlaybackStateChangedData) => { const onPlaybackStateChanged = (data: OnPlaybackStateChangedData) => {
console.log('onPlaybackStateChanged', data); console.log('onPlaybackStateChanged', data);
}; };
toggleFullscreen() { const goToChannel = (channel: number) => {
this.setState({fullscreen: !this.state.fullscreen}); setState({
} ...state,
toggleControls() {
this.setState({showRNVControls: !this.state.showRNVControls});
}
toggleDecoration() {
this.setState({decoration: !this.state.decoration});
this.video?.setFullScreen(!this.state.decoration);
}
toggleShowNotificationControls() {
this.setState({
showNotificationControls: !this.state.showNotificationControls,
});
}
goToChannel(channel: number) {
this.setState({
srcListId: channel, srcListId: channel,
duration: 0.0, duration: 0.0,
currentTime: 0.0, currentTime: 0.0,
@ -462,370 +189,79 @@ class VideoPlayer extends Component {
type: SelectedVideoTrackType.AUTO, type: SelectedVideoTrackType.AUTO,
}, },
}); });
} };
channelUp() { const channelUp = () => {
console.log('channel up'); console.log('channel up');
this.goToChannel((this.state.srcListId + 1) % this.srcList.length); goToChannel((state.srcListId + 1) % srcList.length);
} };
channelDown() { const channelDown = () => {
console.log('channel down'); console.log('channel down');
this.goToChannel( goToChannel((state.srcListId + srcList.length - 1) % srcList.length);
(this.state.srcListId + this.srcList.length - 1) % this.srcList.length, };
);
}
videoSeek(position: number) { return (
this.setState({isSeeking: true}); <View style={styles.container}>
this.video?.seek(position); {(srcList[state.srcListId] as AdditionalSourceInfo)?.noView ? null : (
} <TouchableOpacity style={viewStyle}>
<Video
renderSeekBar() { showNotificationControls={state.showNotificationControls}
return ( ref={videoRef}
<Seeker source={currentSrc as ReactVideoSource}
currentTime={this.state.currentTime} textTracks={additional?.textTracks}
duration={this.state.duration} adTagUrl={additional?.adTagUrl}
isLoading={this.state.isLoading} drm={additional?.drm}
videoSeek={prop => this.videoSeek(prop)} style={viewStyle}
isUISeeking={this.state.isSeeking} rate={state.rate}
paused={state.paused}
volume={state.volume}
muted={state.muted}
fullscreen={state.fullscreen}
controls={state.showRNVControls}
resizeMode={state.resizeMode}
onLoad={onLoad}
onAudioTracks={onAudioTracks}
onTextTracks={onTextTracks}
onVideoTracks={onVideoTracks}
onTextTrackDataChanged={onTextTrackDataChanged}
onProgress={onProgress}
onEnd={onEnd}
progressUpdateInterval={1000}
onError={onError}
onAudioBecomingNoisy={onAudioBecomingNoisy}
onAudioFocusChanged={onAudioFocusChanged}
onLoadStart={onVideoLoadStart}
onAspectRatio={onAspectRatio}
onReadyForDisplay={onReadyForDisplay}
onBuffer={onVideoBuffer}
onSeek={onSeek}
repeat={state.loop}
selectedTextTrack={state.selectedTextTrack}
selectedAudioTrack={state.selectedAudioTrack}
selectedVideoTrack={state.selectedVideoTrack}
playInBackground={false}
bufferConfig={{
...bufferConfig,
cacheSizeMB: state.useCache ? 200 : 0,
}}
preventsDisplaySleepDuringVideoPlayback={true}
poster={state.poster}
onPlaybackRateChange={onPlaybackRateChange}
onPlaybackStateChanged={onPlaybackStateChanged}
bufferingStrategy={BufferingStrategyType.DEFAULT}
debug={{enable: true, thread: true}}
/>
</TouchableOpacity>
)}
<Overlay
channelDown={channelDown}
channelUp={channelUp}
setState={setState}
state={state}
ref={videoRef}
/> />
); </View>
} );
};
IndicatorLoadingView() {
if (this.state.isLoading) {
return (
<ActivityIndicator
color="#3235fd"
size="large"
style={styles.IndicatorStyle}
/>
);
} else {
return <View />;
}
}
renderTopControl() {
return (
<View style={styles.topControlsContainer}>
<Text style={styles.controlOption}>
{(this.srcList[this.state.srcListId] as AdditionnalSourceInfo)
?.description || 'local file'}
</Text>
<View>
<TouchableOpacity
onPress={() => {
this.toggleControls();
}}>
<Text style={styles.leftRightControlOption}>
{this.state.showRNVControls ? 'Hide controls' : 'Show controls'}
</Text>
</TouchableOpacity>
</View>
</View>
);
}
onRateSelected = (value: MultiValueControlPropType) => {
this.setState({rate: value});
};
onVolumeSelected = (value: MultiValueControlPropType) => {
this.setState({volume: value});
};
onResizeModeSelected = (value: MultiValueControlPropType) => {
this.setState({resizeMode: value});
};
onSelectedAudioTrackChange = (itemValue: string) => {
console.log('on audio value change ' + itemValue);
if (itemValue === 'none') {
this.setState({
selectedAudioTrack: SelectedVideoTrackType.DISABLED,
});
} else {
this.setState({
selectedAudioTrack: {
type: SelectedVideoTrackType.INDEX,
value: itemValue,
},
});
}
};
onSelectedTextTrackChange = (itemValue: string) => {
console.log('on value change ' + itemValue);
this.setState({
selectedTextTrack: {
type: this.textTracksSelectionBy === 'index' ? 'index' : 'language',
value: itemValue,
},
});
};
onSelectedVideoTrackChange = (itemValue: string) => {
console.log('on value change ' + itemValue);
if (itemValue === undefined || itemValue === 'auto') {
this.setState({
selectedVideoTrack: {
type: SelectedVideoTrackType.AUTO,
},
});
} else {
this.setState({
selectedVideoTrack: {
type: SelectedVideoTrackType.INDEX,
value: itemValue,
},
});
}
};
renderOverlay() {
return (
<>
{this.IndicatorLoadingView()}
<View style={styles.topControls}>
<View style={styles.resizeModeControl}>
{this.renderTopControl()}
</View>
</View>
{!this.state.showRNVControls ? (
<>
<View style={styles.leftControls}>
<ToggleControl
onPress={() => {
this.channelDown();
}}
text="ChDown"
/>
</View>
<View style={styles.rightControls}>
<ToggleControl
onPress={() => {
this.channelUp();
}}
text="ChUp"
/>
</View>
<View style={styles.bottomControls}>
<View style={styles.generalControls}>
{Platform.OS === 'android' ? (
<View style={styles.generalControls}>
<ToggleControl
onPress={() => {
this.popupInfo();
}}
text="decoderInfo"
/>
<ToggleControl
isSelected={this.state.useCache}
onPress={() => {
this.setState({useCache: !this.state.useCache});
}}
selectedText="enable cache"
unselectedText="disable cache"
/>
</View>
) : null}
<ToggleControl
isSelected={this.state.paused}
onPress={() => {
this.setState({paused: !this.state.paused});
}}
selectedText="pause"
unselectedText="playing"
/>
<ToggleControl
isSelected={this.state.loop}
onPress={() => {
this.setState({loop: !this.state.loop});
}}
selectedText="loop enable"
unselectedText="loop disable"
/>
<ToggleControl
onPress={() => {
this.toggleFullscreen();
}}
text="fullscreen"
/>
<ToggleControl
onPress={() => {
this.toggleDecoration();
}}
text="decoration"
/>
<ToggleControl
isSelected={!!this.state.poster}
onPress={() => {
this.setState({
poster: this.state.poster ? undefined : this.samplePoster,
});
}}
selectedText="poster"
unselectedText="no poster"
/>
<ToggleControl
isSelected={this.state.showNotificationControls}
onPress={() => {
this.toggleShowNotificationControls();
}}
selectedText="hide notification controls"
unselectedText="show notification controls"
/>
</View>
<View style={styles.generalControls}>
{/* shall be replaced by slider */}
<MultiValueControl
values={[0, 0.25, 0.5, 1.0, 1.5, 2.0]}
onPress={this.onRateSelected}
selected={this.state.rate}
/>
{/* shall be replaced by slider */}
<MultiValueControl
values={[0.5, 1, 1.5]}
onPress={this.onVolumeSelected}
selected={this.state.volume}
/>
<MultiValueControl
values={[
ResizeMode.COVER,
ResizeMode.CONTAIN,
ResizeMode.STRETCH,
]}
onPress={this.onResizeModeSelected}
selected={this.state.resizeMode}
/>
<ToggleControl
isSelected={this.state.muted}
onPress={() => {
this.setState({muted: !this.state.muted});
}}
text="muted"
/>
{Platform.OS === 'ios' ? (
<ToggleControl
isSelected={this.state.paused}
onPress={() => {
this.video
?.save({})
?.then(response => {
console.log('Downloaded URI', response);
})
.catch(error => {
console.log('error during save ', error);
});
}}
text="save"
/>
) : null}
</View>
{this.renderSeekBar()}
<View style={styles.generalControls}>
<AudioTrackSelector
audioTracks={this.state.audioTracks}
selectedAudioTrack={this.state.selectedAudioTrack}
onValueChange={this.onSelectedAudioTrackChange}
/>
<TextTrackSelector
textTracks={this.state.textTracks}
selectedTextTrack={this.state.selectedTextTrack}
onValueChange={this.onSelectedTextTrackChange}
textTracksSelectionBy={this.textTracksSelectionBy}
/>
<VideoTrackSelector
videoTracks={this.state.videoTracks}
selectedVideoTrack={this.state.selectedVideoTrack}
onValueChange={this.onSelectedVideoTrackChange}
/>
</View>
</View>
</>
) : null}
</>
);
}
renderVideoView() {
const viewStyle = this.state.fullscreen
? styles.fullScreen
: styles.halfScreen;
const currentSrc = this.srcList[this.state.srcListId];
const additionnal = currentSrc as AdditionnalSourceInfo;
return (
<TouchableOpacity style={viewStyle}>
<Video
showNotificationControls={this.state.showNotificationControls}
ref={(ref: VideoRef) => {
this.video = ref;
}}
source={currentSrc as ReactVideoSource}
textTracks={additionnal?.textTracks}
adTagUrl={additionnal?.adTagUrl}
drm={additionnal?.drm}
style={viewStyle}
rate={this.state.rate}
paused={this.state.paused}
volume={this.state.volume}
muted={this.state.muted}
fullscreen={this.state.fullscreen}
controls={this.state.showRNVControls}
resizeMode={this.state.resizeMode}
onLoad={this.onLoad}
onAudioTracks={this.onAudioTracks}
onTextTracks={this.onTextTracks}
onVideoTracks={this.onVideoTracks}
onTextTrackDataChanged={this.onTextTrackDataChanged}
onProgress={this.onProgress}
onEnd={this.onEnd}
progressUpdateInterval={1000}
onError={this.onError}
onAudioBecomingNoisy={this.onAudioBecomingNoisy}
onAudioFocusChanged={this.onAudioFocusChanged}
onLoadStart={this.onVideoLoadStart}
onAspectRatio={this.onAspectRatio}
onReadyForDisplay={this.onReadyForDisplay}
onBuffer={this.onVideoBuffer}
onSeek={this.onSeek}
repeat={this.state.loop}
selectedTextTrack={this.state.selectedTextTrack}
selectedAudioTrack={this.state.selectedAudioTrack}
selectedVideoTrack={this.state.selectedVideoTrack}
playInBackground={false}
bufferConfig={{
minBufferMs: 15000,
maxBufferMs: 50000,
bufferForPlaybackMs: 2500,
bufferForPlaybackAfterRebufferMs: 5000,
cacheSizeMB: this.state.useCache ? 200 : 0,
live: {
targetOffsetMs: 500,
},
}}
preventsDisplaySleepDuringVideoPlayback={true}
poster={this.state.poster}
onPlaybackRateChange={this.onPlaybackRateChange}
onPlaybackStateChanged={this.onPlaybackStateChanged}
bufferingStrategy={BufferingStrategyType.DEFAULT}
debug={{enable: true, thread: true}}
/>
</TouchableOpacity>
);
}
render() {
return (
<View style={styles.container}>
{(this.srcList[this.state.srcListId] as AdditionnalSourceInfo)?.noView
? null
: this.renderVideoView()}
{this.renderOverlay()}
</View>
);
}
}
export default VideoPlayer; export default VideoPlayer;

View File

@ -108,7 +108,7 @@ class VideoPlayer extends Component {
this.setState({paused: !this.state.paused}); this.setState({paused: !this.state.paused});
}}> }}>
<Video <Video
source={require('./broadchurch.mp4')} source={require('./assets/videos/broadchurch.mp4')}
style={styles.fullScreen} style={styles.fullScreen}
rate={this.state.rate} rate={this.state.rate}
paused={this.state.paused} paused={this.state.paused}

View File

@ -0,0 +1 @@
export * from './videos';

View File

@ -0,0 +1,4 @@
export const localeVideo = {
broadchurch: require('./broadchurch.mp4'),
portrait: require('./portrait.mp4'),
};

View File

@ -10,7 +10,7 @@ export interface AudioTrackSelectorType {
onValueChange: (arg0: string) => void; onValueChange: (arg0: string) => void;
} }
const AudioTrackSelector = ({ export const AudioTrackSelector = ({
audioTracks, audioTracks,
selectedAudioTrack, selectedAudioTrack,
onValueChange, onValueChange,
@ -49,5 +49,3 @@ const AudioTrackSelector = ({
</> </>
); );
}; };
export default AudioTrackSelector;

View File

@ -0,0 +1,22 @@
import React, {FC, memo} from 'react';
import {ActivityIndicator, View} from 'react-native';
import styles from '../styles.tsx';
type Props = {
isLoading: boolean;
};
const _Indicator: FC<Props> = ({isLoading}) => {
if (!isLoading) {
return <View />;
}
return (
<ActivityIndicator
color="#3235fd"
size="large"
style={styles.IndicatorStyle}
/>
);
};
export const Indicator = memo(_Indicator);

View File

@ -0,0 +1,310 @@
import React, {forwardRef, memo, useCallback} from 'react';
import {Indicator} from './Indicator.tsx';
import {View} from 'react-native';
import styles from '../styles.tsx';
import ToggleControl from '../ToggleControl.tsx';
import {
isAndroid,
isIos,
samplePoster,
textTracksSelectionBy,
} from '../constants';
import MultiValueControl, {
MultiValueControlPropType,
} from '../MultiValueControl.tsx';
import {
ResizeMode,
SelectedTrackType,
SelectedVideoTrackType,
VideoDecoderProperties,
VideoRef,
} from 'react-native-video';
import {StateType} from '../types';
import {
toast,
Seeker,
AudioTrackSelector,
TextTrackSelector,
VideoTrackSelector,
TopControl,
} from '../components';
type Props = {
channelDown: () => void;
channelUp: () => void;
setState: (value: StateType) => void;
state: StateType;
};
const _Overlay = forwardRef<VideoRef, Props>((props, ref) => {
const {state, setState, channelUp, channelDown} = props;
const popupInfo = useCallback(() => {
VideoDecoderProperties.getWidevineLevel().then((widevineLevel: number) => {
VideoDecoderProperties.isHEVCSupported().then((hevc: string) => {
VideoDecoderProperties.isCodecSupported('video/avc', 1920, 1080).then(
(avc: string) => {
toast(
true,
'Widevine level: ' +
widevineLevel +
'\n hevc: ' +
hevc +
'\n avc: ' +
avc,
);
},
);
});
});
}, []);
const toggleFullscreen = () => {
setState({...state, fullscreen: !state.fullscreen});
};
const toggleControls = () => {
setState({...state, showRNVControls: !state.showRNVControls});
};
const toggleDecoration = () => {
setState({...state, decoration: !state.decoration});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
ref.current?.setFullScreen(!state.decoration);
};
const toggleShowNotificationControls = () => {
setState({
...state,
showNotificationControls: !state.showNotificationControls,
});
};
const onSelectedAudioTrackChange = (itemValue: string) => {
console.log('on audio value change ' + itemValue);
if (itemValue === 'none') {
setState({
...state,
selectedAudioTrack: {
type: SelectedTrackType.DISABLED,
},
});
} else {
setState({
...state,
selectedAudioTrack: {
type: SelectedTrackType.INDEX,
value: itemValue,
},
});
}
};
const onSelectedTextTrackChange = (itemValue: string) => {
console.log('on value change ' + itemValue);
setState({
...state,
selectedTextTrack: {
type:
textTracksSelectionBy === 'index'
? SelectedTrackType.INDEX
: SelectedTrackType.LANGUAGE,
value: itemValue,
},
});
};
const onSelectedVideoTrackChange = (itemValue: string) => {
console.log('on value change ' + itemValue);
if (itemValue === undefined || itemValue === 'auto') {
setState({
...state,
selectedVideoTrack: {
type: SelectedVideoTrackType.AUTO,
},
});
} else {
setState({
...state,
selectedVideoTrack: {
type: SelectedVideoTrackType.INDEX,
value: itemValue,
},
});
}
};
const videoSeek = (position: number) => {
setState({...state, isSeeking: true});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
ref.current?.seek(position);
};
const onRateSelected = (value: MultiValueControlPropType) => {
if (typeof value === 'number') {
setState({...state, rate: value});
}
};
const onVolumeSelected = (value: MultiValueControlPropType) => {
if (typeof value === 'number') {
setState({...state, volume: value});
}
};
const onResizeModeSelected = (value: MultiValueControlPropType) => {
if (typeof value === 'object') {
setState({...state, resizeMode: value});
}
};
return (
<>
<Indicator isLoading={state.isLoading} />
<View style={styles.topControls}>
<View style={styles.resizeModeControl}>
<TopControl
srcListId={state.srcListId}
showRNVControls={state.showRNVControls}
toggleControls={toggleControls}
/>
</View>
</View>
{!state.showRNVControls ? (
<>
<View style={styles.leftControls}>
<ToggleControl onPress={channelDown} text="ChDown" />
</View>
<View style={styles.rightControls}>
<ToggleControl onPress={channelUp} text="ChUp" />
</View>
<View style={styles.bottomControls}>
<View style={styles.generalControls}>
{isAndroid ? (
<View style={styles.generalControls}>
<ToggleControl onPress={popupInfo} text="decoderInfo" />
<ToggleControl
isSelected={state.useCache}
onPress={() => {
setState({...state, useCache: !state.useCache});
}}
selectedText="enable cache"
unselectedText="disable cache"
/>
</View>
) : null}
<ToggleControl
isSelected={state.paused}
onPress={() => {
setState({...state, paused: !state.paused});
}}
selectedText="pause"
unselectedText="playing"
/>
<ToggleControl
isSelected={state.loop}
onPress={() => {
setState({...state, loop: !state.loop});
}}
selectedText="loop enable"
unselectedText="loop disable"
/>
<ToggleControl onPress={toggleFullscreen} text="fullscreen" />
<ToggleControl onPress={toggleDecoration} text="decoration" />
<ToggleControl
isSelected={!!state.poster}
onPress={() => {
setState({
...state,
poster: state.poster ? undefined : samplePoster,
});
}}
selectedText="poster"
unselectedText="no poster"
/>
<ToggleControl
isSelected={state.showNotificationControls}
onPress={toggleShowNotificationControls}
selectedText="hide notification controls"
unselectedText="show notification controls"
/>
</View>
<View style={styles.generalControls}>
{/* shall be replaced by slider */}
<MultiValueControl
values={[0, 0.25, 0.5, 1.0, 1.5, 2.0]}
onPress={onRateSelected}
selected={state.rate}
/>
{/* shall be replaced by slider */}
<MultiValueControl
values={[0.5, 1, 1.5]}
onPress={onVolumeSelected}
selected={state.volume}
/>
<MultiValueControl
values={[
ResizeMode.COVER,
ResizeMode.CONTAIN,
ResizeMode.STRETCH,
]}
onPress={onResizeModeSelected}
selected={state.resizeMode}
/>
<ToggleControl
isSelected={state.muted}
onPress={() => {
setState({...state, muted: !state.muted});
}}
text="muted"
/>
{isIos ? (
<ToggleControl
isSelected={state.paused}
onPress={() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
ref.current
?.save({})
?.then((response: unknown) => {
console.log('Downloaded URI', response);
})
.catch((error: unknown) => {
console.log('error during save ', error);
});
}}
text="save"
/>
) : null}
</View>
<Seeker
currentTime={state.currentTime}
duration={state.duration}
isLoading={state.isLoading}
videoSeek={prop => videoSeek(prop)}
isUISeeking={state.isSeeking}
/>
<View style={styles.generalControls}>
<AudioTrackSelector
audioTracks={state.audioTracks}
selectedAudioTrack={state.selectedAudioTrack}
onValueChange={onSelectedAudioTrackChange}
/>
<TextTrackSelector
textTracks={state.textTracks}
selectedTextTrack={state.selectedTextTrack}
onValueChange={onSelectedTextTrackChange}
textTracksSelectionBy={textTracksSelectionBy}
/>
<VideoTrackSelector
videoTracks={state.videoTracks}
selectedVideoTrack={state.selectedVideoTrack}
onValueChange={onSelectedVideoTrackChange}
/>
</View>
</View>
</>
) : null}
</>
);
});
export const Overlay = memo(_Overlay);

View File

@ -10,7 +10,7 @@ interface SeekerProps {
videoSeek: (arg0: number) => void; videoSeek: (arg0: number) => void;
} }
const Seeker = ({ export const Seeker = ({
currentTime, currentTime,
duration, duration,
isLoading, isLoading,
@ -150,5 +150,3 @@ const Seeker = ({
</View> </View>
); );
}; };
export default Seeker;

View File

@ -11,7 +11,7 @@ export interface TextTrackSelectorType {
textTracksSelectionBy: string; textTracksSelectionBy: string;
} }
const TextTrackSelector = ({ export const TextTrackSelector = ({
textTracks, textTracks,
selectedTextTrack, selectedTextTrack,
onValueChange, onValueChange,
@ -60,5 +60,3 @@ const TextTrackSelector = ({
</> </>
); );
}; };
export default TextTrackSelector;

View File

@ -0,0 +1,18 @@
import {Alert, ToastAndroid} from 'react-native';
import {isAndroid} from '../constants';
export const toast = (visible: boolean, message: string) => {
if (visible) {
if (isAndroid) {
ToastAndroid.showWithGravityAndOffset(
message,
ToastAndroid.LONG,
ToastAndroid.BOTTOM,
25,
50,
);
} else {
Alert.alert(message, message);
}
}
};

View File

@ -0,0 +1,37 @@
import React, {FC, memo} from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
import styles from '../styles.tsx';
import {srcList} from '../constants';
import {AdditionalSourceInfo} from '../types';
type Props = {
srcListId: number;
showRNVControls: boolean;
toggleControls: () => void;
};
const _TopControl: FC<Props> = ({
toggleControls,
showRNVControls,
srcListId,
}) => {
return (
<View style={styles.topControlsContainer}>
<Text style={styles.controlOption}>
{(srcList[srcListId] as AdditionalSourceInfo)?.description ||
'local file'}
</Text>
<View>
<TouchableOpacity
onPress={() => {
toggleControls();
}}>
<Text style={styles.leftRightControlOption}>
{showRNVControls ? 'Hide controls' : 'Show controls'}
</Text>
</TouchableOpacity>
</View>
</View>
);
};
export const TopControl = memo(_TopControl);

View File

@ -14,7 +14,7 @@ export interface VideoTrackSelectorType {
onValueChange: (arg0: string) => void; onValueChange: (arg0: string) => void;
} }
const VideoTrackSelector = ({ export const VideoTrackSelector = ({
videoTracks, videoTracks,
selectedVideoTrack, selectedVideoTrack,
onValueChange, onValueChange,
@ -60,5 +60,3 @@ const VideoTrackSelector = ({
</> </>
); );
}; };
export default VideoTrackSelector;

View File

@ -0,0 +1,8 @@
export * from './Indicator';
export * from './Seeker';
export * from './AudioTracksSelector';
export * from './VideoTracksSelector';
export * from './TextTracksSelector';
export * from './Overlay';
export * from './TopControl';
export * from './Toast';

View File

@ -0,0 +1,200 @@
import {
BufferConfig,
DRMType,
ISO639_1,
ResizeMode,
SelectedVideoTrackType,
TextTrackType,
} from 'react-native-video';
import {SampleVideoSource, StateType} from '../types';
import {localeVideo} from '../assets';
import {Platform} from 'react-native';
export const textTracksSelectionBy = 'index';
export const isIos = Platform.OS === 'ios';
export const isAndroid = Platform.OS === 'android';
export const srcAllPlatformList = [
{
description: 'local file landscape',
uri: localeVideo.broadchurch,
},
{
description: 'local file landscape cropped',
uri: localeVideo.broadchurch,
cropStart: 3000,
cropEnd: 10000,
},
{
description: 'local file portrait',
uri: localeVideo.portrait,
metadata: {
title: 'Test Title',
subtitle: 'Test Subtitle',
artist: 'Test Artist',
description: 'Test Description',
imageUri:
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
},
},
{
description: '(hls|live) red bull tv',
textTracksAllowChunklessPreparation: false,
uri: 'https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_928.m3u8',
metadata: {
title: 'Custom Title',
subtitle: 'Custom Subtitle',
artist: 'Custom Artist',
description: 'Custom Description',
imageUri:
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
},
},
{
description: 'invalid URL',
uri: 'mmt://www.youtube.com',
type: 'mpd',
},
{description: '(no url) Stopped playback', uri: undefined},
{
description: '(no view) no View',
noView: true,
},
{
description: 'Another live sample',
uri: 'https://live.forstreet.cl/live/livestream.m3u8',
},
{
description: 'another bunny (can be saved)',
uri: 'https://rawgit.com/mediaelement/mediaelement-files/master/big_buck_bunny.mp4',
headers: {referer: 'www.github.com', 'User-Agent': 'react.native.video'},
},
{
description: 'sintel with subtitles',
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
},
{
description: 'sintel starts at 20sec',
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
startPosition: 50000,
},
{
description: 'BigBugBunny sideLoaded subtitles',
// sideloaded subtitles wont work for streaming like HLS on ios
// mp4
uri: 'https://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
textTracks: [
{
title: 'test',
language: 'en' as ISO639_1,
type: TextTrackType.VTT,
uri: 'https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt',
},
],
},
];
export const srcIosList = [];
export const srcAndroidList = [
{
description: 'Another live sample',
uri: 'https://live.forstreet.cl/live/livestream.m3u8',
},
{
description: 'asset file',
uri: 'asset:///broadchurch.mp4',
},
{
description: '(dash) sintel subtitles',
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/sintel.mpd',
},
{
description: '(mp4) big buck bunny',
uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
},
{
description: '(mp4|subtitles) demo with sintel Subtitles',
uri: 'http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0',
type: 'mpd',
},
{
description: '(mp4) big buck bunny With Ads',
adTagUrl:
'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostoptimizedpodbumper&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator=',
uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
},
{
description: 'WV: Secure SD & HD (cbcs,MP4,H264)',
uri: 'https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd',
drm: {
type: DRMType.WIDEVINE,
licenseServer:
'https://proxy.uat.widevine.com/proxy?provider=widevine_test',
},
},
{
description: 'Secure UHD (cenc)',
uri: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd',
drm: {
type: DRMType.WIDEVINE,
licenseServer:
'https://proxy.uat.widevine.com/proxy?provider=widevine_test',
},
},
{
description: 'rtsp big bug bunny',
uri: 'rtsp://rtspstream:3cfa3c36a9c00f4aa38f3cd35816b287@zephyr.rtsp.stream/movie',
type: 'rtsp',
},
];
// poster which can be displayed
export const samplePoster =
'https://upload.wikimedia.org/wikipedia/commons/1/18/React_Native_Logo.png';
export const srcList: SampleVideoSource[] = srcAllPlatformList.concat(
isAndroid ? srcAndroidList : srcIosList,
);
export const defaultValue: StateType = {
rate: 1,
volume: 1,
muted: false,
resizeMode: ResizeMode.CONTAIN,
duration: 0.0,
currentTime: 0.0,
videoWidth: 0,
videoHeight: 0,
paused: false,
fullscreen: true,
decoration: true,
isLoading: false,
audioTracks: [],
textTracks: [],
videoTracks: [],
selectedAudioTrack: undefined,
selectedTextTrack: undefined,
selectedVideoTrack: {
type: SelectedVideoTrackType.AUTO,
},
srcListId: 0,
loop: false,
showRNVControls: false,
useCache: false,
poster: undefined,
showNotificationControls: false,
isSeeking: false,
};
export const bufferConfig: BufferConfig = {
minBufferMs: 15000,
maxBufferMs: 50000,
bufferForPlaybackMs: 2500,
bufferForPlaybackAfterRebufferMs: 5000,
live: {
targetOffsetMs: 500,
},
}

View File

@ -0,0 +1 @@
export * from './general';

View File

@ -0,0 +1 @@
export * from './types';

View File

@ -0,0 +1,49 @@
import {
AudioTrack,
Drm,
ReactVideoSource,
ResizeMode,
SelectedTrack,
SelectedVideoTrack,
TextTrack,
TextTracks,
VideoTrack,
} from 'react-native-video';
export type AdditionalSourceInfo = {
textTracks: TextTracks;
adTagUrl: string;
description: string;
drm: Drm;
noView: boolean;
};
export type SampleVideoSource = ReactVideoSource | AdditionalSourceInfo;
export interface StateType {
rate: number;
volume: number;
muted: boolean;
resizeMode: ResizeMode;
duration: number;
currentTime: number;
videoWidth: number;
videoHeight: number;
paused: boolean;
fullscreen: boolean;
decoration: boolean;
isLoading: boolean;
audioTracks: Array<AudioTrack>;
textTracks: Array<TextTrack>;
videoTracks: Array<VideoTrack>;
selectedAudioTrack: SelectedTrack | undefined;
selectedTextTrack: SelectedTrack | undefined;
selectedVideoTrack: SelectedVideoTrack;
srcListId: number;
loop: boolean;
showRNVControls: boolean;
useCache: boolean;
poster?: string;
showNotificationControls: boolean;
isSeeking: boolean;
}