feat(sample): merge ios and android samples (#3015)

* chore: split components
This commit is contained in:
Olivier Bouillet 2023-10-07 23:14:09 +02:00 committed by GitHub
parent a855284d8d
commit 1f0137608a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 463 additions and 855 deletions

View File

@ -75,9 +75,9 @@ PODS:
- hermes-engine/Pre-built (0.72.5) - hermes-engine/Pre-built (0.72.5)
- libevent (2.1.12) - libevent (2.1.12)
- OpenSSL-Universal (1.1.1100) - OpenSSL-Universal (1.1.1100)
- PromisesObjC (2.3.1) - PromisesObjC (2.2.0)
- PromisesSwift (2.3.1): - PromisesSwift (2.2.0):
- PromisesObjC (= 2.3.1) - PromisesObjC (= 2.2.0)
- RCT-Folly (2021.07.22.00): - RCT-Folly (2021.07.22.00):
- boost - boost
- DoubleConversion - DoubleConversion
@ -691,8 +691,8 @@ SPEC CHECKSUMS:
hermes-engine: f6cf92a471053245614d9d8097736f6337d5b86c hermes-engine: f6cf92a471053245614d9d8097736f6337d5b86c
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265 PromisesSwift: cf9eb58666a43bbe007302226e510b16c1e10959
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: df81ab637d35fac9e6eb94611cfd20f0feb05455 RCTRequired: df81ab637d35fac9e6eb94611cfd20f0feb05455
RCTTypeSafety: 4636e4a36c7c2df332bda6d59b19b41c443d4287 RCTTypeSafety: 4636e4a36c7c2df332bda6d59b19b41c443d4287
@ -733,4 +733,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 6899e375fcfa0d3a42aa6cb55266008b8f7419cb PODFILE CHECKSUM: 6899e375fcfa0d3a42aa6cb55266008b8f7419cb
COCOAPODS: 1.13.0 COCOAPODS: 1.12.1

View File

@ -602,10 +602,7 @@
); );
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-Wl",
"-ld_classic",
" ", " ",
"-Wl -ld_classic ",
); );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -678,10 +675,7 @@
); );
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-Wl",
"-ld_classic",
" ", " ",
"-Wl -ld_classic ",
); );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos; SDKROOT = iphoneos;

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,67 @@
import React from 'react';
import {
StyleSheet,
Text,
TextStyle,
TouchableOpacity,
View,
} from 'react-native';
/*
* MultiValueControl displays a list clickable text view
*/
interface MultiValueControlType {
// a list a string or number to be displayed
values: Array<string | number>
// The selected value in values
selected?: string | number
// callback to press onPress
onPress: (arg: string | number) => any
}
const MultiValueControl = ({ values, selected, onPress }: MultiValueControlType) => {
const selectedStyle: TextStyle = StyleSheet.flatten([
styles.option,
{fontWeight: 'bold'},
]);
const unselectedStyle: TextStyle = StyleSheet.flatten([
styles.option,
{fontWeight: 'normal'},
]);
return <View style={styles.container}>
{values.map((value: string | number) => {
const _style = value === selected ? selectedStyle : unselectedStyle
return (
<TouchableOpacity
key={value}
onPress={() => {
onPress?.(value)
}}>
<Text style={_style}>{value}</Text>
</TouchableOpacity>)
})}
</View>
}
const styles = StyleSheet.create({
option: {
alignSelf: 'center',
fontSize: 11,
color: 'white',
paddingLeft: 2,
paddingRight: 2,
lineHeight: 12,
},
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
});
export default MultiValueControl;

View File

@ -0,0 +1,68 @@
import React from 'react';
import {
StyleSheet,
Text,
TextStyle,
TouchableOpacity,
View,
} from 'react-native';
/*
* ToggleControl displays a 2 states clickable text
*/
interface ToggleControlType {
// boolean indicating if text is selected state
isSelected?: boolean
// value of text when selected
selectedText?: string
// value of text when NOT selected
unselectedText?: string
// default text if no only one text field is needed
text?: string
// callback called when pressing the component
onPress: () => any
}
const ToggleControl = ({ isSelected, selectedText, unselectedText, text, onPress }: ToggleControlType) => {
const selectedStyle: TextStyle = StyleSheet.flatten([
styles.controlOption,
{fontWeight: 'bold'},
]);
const unselectedStyle: TextStyle = StyleSheet.flatten([
styles.controlOption,
{fontWeight: 'normal'},
]);
const style = isSelected ? selectedStyle : unselectedStyle;
const _text = text ? text : isSelected ? selectedText : unselectedText;
return (
<View style={styles.resizeModeControl}>
<TouchableOpacity
onPress={onPress}>
<Text style={style}>{_text}</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
controlOption: {
alignSelf: 'center',
fontSize: 11,
color: 'white',
paddingLeft: 2,
paddingRight: 2,
lineHeight: 12,
},
resizeModeControl: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
});
export default ToggleControl;

View File

@ -1,475 +0,0 @@
'use strict';
import React, {
Component
} from 'react';
import {
Alert,
Platform,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Video,{FilterType} from 'react-native-video';
const filterTypes = [
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
];
class VideoPlayer extends Component {
constructor(props: any) {
super(props);
this.onLoad = this.onLoad.bind(this);
this.onProgress = this.onProgress.bind(this);
this.onBuffer = this.onBuffer.bind(this);
}
video = React.createRef();
state = {
rate: 1,
volume: 1,
muted: false,
resizeMode: 'contain',
duration: 0.0,
currentTime: 0.0,
controls: false,
paused: true,
skin: 'custom',
ignoreSilentSwitch: null,
mixWithOthers: null,
isBuffering: false,
filter: FilterType.NONE,
filterEnabled: true
};
onLoad(data: any) {
console.log('On load fired!');
this.setState({duration: data.duration});
}
onProgress(data: any) {
this.setState({currentTime: data.currentTime});
}
onBuffer({ isBuffering }: { isBuffering: boolean }) {
this.setState({ isBuffering });
}
getCurrentTimePercentage() {
if (this.state.currentTime > 0 && this.state.duration !== 0) {
return this.state.currentTime / this.state.duration;
} else {
return 0;
}
}
setFilter(step: number) {
let index = filterTypes.indexOf(this.state.filter) + step;
if (index === filterTypes.length) {
index = 0;
} else if (index === -1) {
index = filterTypes.length - 1;
}
this.setState({
filter: filterTypes[index]
})
}
renderSkinControl(skin) {
const isSelected = this.state.skin == skin;
const selectControls = skin == 'native' || skin == 'embed';
return (
<TouchableOpacity onPress={() => { this.setState({
controls: selectControls,
skin: skin
}) }}>
<Text style={[styles.controlOption, {fontWeight: isSelected ? "bold" : "normal"}]}>
{skin}
</Text>
</TouchableOpacity>
);
}
renderRateControl(rate: number) {
const isSelected = (this.state.rate == rate);
return (
<TouchableOpacity onPress={() => { this.setState({rate: rate}) }}>
<Text style={[styles.controlOption, {fontWeight: isSelected ? "bold" : "normal"}]}>
{rate}x
</Text>
</TouchableOpacity>
)
}
renderResizeModeControl(resizeMode: string) {
const isSelected = (this.state.resizeMode == resizeMode);
return (
<TouchableOpacity onPress={() => { this.setState({resizeMode: resizeMode}) }}>
<Text style={[styles.controlOption, {fontWeight: isSelected ? "bold" : "normal"}]}>
{resizeMode}
</Text>
</TouchableOpacity>
)
}
renderVolumeControl(volume: number) {
const isSelected = (this.state.volume == volume);
return (
<TouchableOpacity onPress={() => { this.setState({volume: volume}) }}>
<Text style={[styles.controlOption, {fontWeight: isSelected ? "bold" : "normal"}]}>
{volume * 100}%
</Text>
</TouchableOpacity>
)
}
renderIgnoreSilentSwitchControl(ignoreSilentSwitch: string) {
const isSelected = (this.state.ignoreSilentSwitch == ignoreSilentSwitch);
return (
<TouchableOpacity onPress={() => { this.setState({ignoreSilentSwitch: ignoreSilentSwitch}) }}>
<Text style={[styles.controlOption, {fontWeight: isSelected ? "bold" : "normal"}]}>
{ignoreSilentSwitch}
</Text>
</TouchableOpacity>
)
}
renderFullscreenControl(fullscreen: string) {
return (
<TouchableOpacity onPress={() => {
if (fullscreen === 'fullscreen') {
this.video.presentFullscreenPlayer()
}
}}>
<Text style={[styles.controlOption]}>
{fullscreen}
</Text>
</TouchableOpacity>
)
}
renderMixWithOthersControl(mixWithOthers: string) {
const isSelected = (this.state.mixWithOthers == mixWithOthers);
return (
<TouchableOpacity onPress={() => { this.setState({mixWithOthers: mixWithOthers}) }}>
<Text style={[styles.controlOption, {fontWeight: isSelected ? "bold" : "normal"}]}>
{mixWithOthers}
</Text>
</TouchableOpacity>
)
}
renderCustomSkin() {
const flexCompleted = this.getCurrentTimePercentage() * 100;
const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100;
return (
<View style={styles.container}>
<TouchableOpacity style={styles.fullScreen} onPress={() => {this.setState({paused: !this.state.paused})}}>
<Video
ref={(ref: Video) => {
this.video = ref
}}
source={require('./broadchurch.mp4')}
style={styles.fullScreen}
rate={this.state.rate}
paused={this.state.paused}
volume={this.state.volume}
muted={this.state.muted}
ignoreSilentSwitch={this.state.ignoreSilentSwitch}
mixWithOthers={this.state.mixWithOthers}
resizeMode={this.state.resizeMode}
onLoad={this.onLoad}
onBuffer={this.onBuffer}
onProgress={this.onProgress}
onEnd={() => { Alert.alert('Done!') }}
repeat={true}
filter={this.state.filter}
filterEnabled={this.state.filterEnabled}
/>
</TouchableOpacity>
<View style={styles.controls}>
<View style={styles.generalControls}>
<View style={styles.skinControl}>
{this.renderSkinControl('custom')}
{this.renderSkinControl('native')}
{this.renderSkinControl('embed')}
</View>
{
(this.state.filterEnabled) ?
<View style={styles.skinControl}>
<TouchableOpacity onPress={() => {
this.setFilter(-1)
}}>
<Text style={styles.controlOption}>Previous Filter</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => {
this.setFilter(1)
}}>
<Text style={styles.controlOption}>Next Filter</Text>
</TouchableOpacity>
</View> : null
}
</View>
<View style={styles.generalControls}>
<View style={styles.rateControl}>
{this.renderRateControl(0.5)}
{this.renderRateControl(1.0)}
{this.renderRateControl(2.0)}
</View>
<View style={styles.volumeControl}>
{this.renderVolumeControl(0.5)}
{this.renderVolumeControl(1)}
{this.renderVolumeControl(1.5)}
</View>
<View style={styles.resizeModeControl}>
{this.renderResizeModeControl('cover')}
{this.renderResizeModeControl('contain')}
{this.renderResizeModeControl('stretch')}
</View>
</View>
<View style={styles.generalControls}>
{
(Platform.OS === 'ios') ?
<>
<View style={styles.ignoreSilentSwitchControl}>
{this.renderIgnoreSilentSwitchControl('ignore')}
{this.renderIgnoreSilentSwitchControl('obey')}
</View>
<View style={styles.mixWithOthersControl}>
{this.renderMixWithOthersControl('mix')}
{this.renderMixWithOthersControl('duck')}
</View>
<View style={styles.mixWithOthersControl}>
{this.renderFullscreenControl('fullscreen')}
</View>
</> : null
}
</View>
<View style={styles.trackingControls}>
<View style={styles.progress}>
<View style={[styles.innerProgressCompleted, {flex: flexCompleted}]} />
<View style={[styles.innerProgressRemaining, {flex: flexRemaining}]} />
</View>
</View>
</View>
</View>
);
}
renderNativeSkin() {
const videoStyle = this.state.skin == 'embed' ? styles.nativeVideoControls : styles.fullScreen;
return (
<View style={styles.container}>
<View style={styles.fullScreen}>
<Video
source={require('./broadchurch.mp4')}
style={videoStyle}
rate={this.state.rate}
paused={this.state.paused}
volume={this.state.volume}
muted={this.state.muted}
ignoreSilentSwitch={this.state.ignoreSilentSwitch}
mixWithOthers={this.state.mixWithOthers}
resizeMode={this.state.resizeMode}
onLoad={this.onLoad}
onBuffer={this.onBuffer}
onProgress={this.onProgress}
onEnd={() => { Alert.alert('Done!') }}
repeat={true}
controls={this.state.controls}
filter={this.state.filter}
filterEnabled={this.state.filterEnabled}
/>
</View>
<View style={styles.controls}>
<View style={styles.generalControls}>
<View style={styles.skinControl}>
{this.renderSkinControl('custom')}
{this.renderSkinControl('native')}
{this.renderSkinControl('embed')}
</View>
{
(this.state.filterEnabled) ?
<View style={styles.skinControl}>
<TouchableOpacity onPress={() => {
this.setFilter(-1)
}}>
<Text style={styles.controlOption}>Previous Filter</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => {
this.setFilter(1)
}}>
<Text style={styles.controlOption}>Next Filter</Text>
</TouchableOpacity>
</View> : null
}
</View>
<View style={styles.generalControls}>
<View style={styles.rateControl}>
{this.renderRateControl(0.5)}
{this.renderRateControl(1.0)}
{this.renderRateControl(2.0)}
</View>
<View style={styles.volumeControl}>
{this.renderVolumeControl(0.5)}
{this.renderVolumeControl(1)}
{this.renderVolumeControl(1.5)}
</View>
<View style={styles.resizeModeControl}>
{this.renderResizeModeControl('cover')}
{this.renderResizeModeControl('contain')}
{this.renderResizeModeControl('stretch')}
</View>
</View>
<View style={styles.generalControls}>
{
(Platform.OS === 'ios') ?
<>
<View style={styles.ignoreSilentSwitchControl}>
{this.renderIgnoreSilentSwitchControl('ignore')}
{this.renderIgnoreSilentSwitchControl('obey')}
</View>
<View style={styles.mixWithOthersControl}>
{this.renderMixWithOthersControl('mix')}
{this.renderMixWithOthersControl('duck')}
</View>
</> : null
}
</View>
</View>
</View>
);
}
render() {
return this.state.controls ? this.renderNativeSkin() : this.renderCustomSkin();
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'black',
},
fullScreen: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
controls: {
backgroundColor: "transparent",
borderRadius: 5,
position: 'absolute',
bottom: 44,
left: 4,
right: 4,
},
progress: {
flex: 1,
flexDirection: 'row',
borderRadius: 3,
overflow: 'hidden',
},
innerProgressCompleted: {
height: 20,
backgroundColor: '#cccccc',
},
innerProgressRemaining: {
height: 20,
backgroundColor: '#2C2C2C',
},
generalControls: {
flex: 1,
flexDirection: 'row',
overflow: 'hidden',
paddingBottom: 10,
},
skinControl: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
rateControl: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
volumeControl: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
resizeModeControl: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
ignoreSilentSwitchControl: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
mixWithOthersControl: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
controlOption: {
alignSelf: 'center',
fontSize: 11,
color: "white",
paddingLeft: 2,
paddingRight: 2,
lineHeight: 12,
},
nativeVideoControls: {
top: 184,
height: 300,
},
trackingControls: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
});
export default VideoPlayer

View File

@ -1,8 +1,6 @@
'use strict'; 'use strict';
import React, { import React, {Component} from 'react';
Component
} from 'react';
import { import {
StyleSheet, StyleSheet,
@ -12,14 +10,18 @@ import {
ActivityIndicator, ActivityIndicator,
PanResponder, PanResponder,
ToastAndroid, ToastAndroid,
Platform,
PanResponderInstance,
Alert,
} from 'react-native'; } from 'react-native';
import { Picker } from '@react-native-picker/picker' import {Picker} from '@react-native-picker/picker';
import Video, { VideoDecoderProperties, TextTrackType } from 'react-native-video'; import Video, {VideoDecoderProperties} from 'react-native-video';
import ToggleControl from './ToggleControl';
import MultiValueControl from './MultiValueControl';
class VideoPlayer extends Component { class VideoPlayer extends Component {
state = { state = {
rate: 1, rate: 1,
volume: 1, volume: 1,
@ -46,10 +48,38 @@ class VideoPlayer extends Component {
showRNVControls: false, showRNVControls: false,
}; };
seekerWidth = 0 seekerWidth = 0;
srcList = [ srcAllPlatformList = [
require('./broadchurch.mp4'), require('./broadchurch.mp4'),
{
description: '(hls|live) red bull tv',
uri: 'https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_928.m3u8',
},
{
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',
},
];
srcIosList = [
]
srcAndroidList = [
{
description: 'Another live sample',
uri: 'https://live.forstreet.cl/live/livestream.m3u8',
},
{ {
description: '(dash) sintel subtitles', description: '(dash) sintel subtitles',
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/sintel.mpd', uri: 'https://bitmovin-a.akamaihd.net/content/sintel/sintel.mpd',
@ -58,31 +88,26 @@ class VideoPlayer extends Component {
description: '(mp4) big buck bunny', description: '(mp4) big buck bunny',
uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4', uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
}, },
{
description: '(hls|live) red bull tv',
uri: 'https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_928.m3u8'
},
{ {
description: '(mp4|subtitles) demo with sintel Subtitles', description: '(mp4|subtitles) demo with sintel Subtitles',
uri: 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',
'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', type: 'mpd',
}, },
{ {
description: 'invalid URL', description: '(mp4) big buck bunny With Ads',
uri: adTagUrl:
'mmt://www.youtube.com', '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=',
type: 'mpd', uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
}, },
{ description: '(no url) Stopped playback', uri: undefined }, ];
{
description: '(no view) no View',
noView: true,
},
]
video: Video;
seekPanResponder: PanResponder | undefined; srcList = this.srcAllPlatformList.concat(
Platform.OS === 'android' ? this.srcAndroidList : this.srcIosList,
);
video?: Video;
seekPanResponder?: PanResponderInstance;
popupInfo = () => { popupInfo = () => {
VideoDecoderProperties.getWidevineLevel().then((widevineLevel: number) => { VideoDecoderProperties.getWidevineLevel().then((widevineLevel: number) => {
@ -105,52 +130,49 @@ class VideoPlayer extends Component {
}; };
onLoad = (data: any) => { onLoad = (data: any) => {
this.setState({ duration: data.duration, loading: false, }); this.setState({duration: data.duration, loading: false});
this.onAudioTracks(data) this.onAudioTracks(data);
this.onTextTracks(data) this.onTextTracks(data);
}; };
onProgress = (data: any) => { onProgress = (data: any) => {
if (!this.state.seeking) { if (!this.state.seeking) {
const position = this.calculateSeekerPosition() const position = this.calculateSeekerPosition();
this.setSeekerPosition(position) this.setSeekerPosition(position);
} }
this.setState({ currentTime: data.currentTime }) this.setState({currentTime: data.currentTime});
}; };
onVideoLoadStart = () => { onVideoLoadStart = () => {
console.log('onVideoLoadStart') console.log('onVideoLoadStart');
this.setState({ isLoading: true }) this.setState({isLoading: true});
} };
onAudioTracks = (data: any) => { onAudioTracks = (data: any) => {
const selectedTrack = data.audioTracks?.find((x: any) => { const selectedTrack = data.audioTracks?.find((x: any) => {
return x.selected return x.selected;
}) });
this.setState({ this.setState({
audioTracks: data.audioTracks, audioTracks: data.audioTracks,
}) });
if (selectedTrack?.language) { if (selectedTrack?.language) {
this.setState({ this.setState({
selectedAudioTrack: { selectedAudioTrack: {
type: 'language', type: 'language',
value: selectedTrack?.language, value: selectedTrack?.language,
}, },
}) });
}
} }
};
onTextTracks = (data: any) => { onTextTracks = (data: any) => {
const selectedTrack = data.textTracks?.find((x: any) => { const selectedTrack = data.textTracks?.find((x: any) => {
return x.selected return x.selected;
}) });
this.setState({ this.setState({
textTracks: data.textTracks, textTracks: data.textTracks,
}) });
if (selectedTrack?.language) { if (selectedTrack?.language) {
this.setState({ this.setState({
textTracks: data, textTracks: data,
@ -158,38 +180,34 @@ class VideoPlayer extends Component {
type: 'language', type: 'language',
value: selectedTrack?.language, value: selectedTrack?.language,
}, },
}) });
}
} }
};
onAspectRatio = (data: any) => { onAspectRatio = (data: any) => {
console.log('onAspectRadio called ' + JSON.stringify(data)) console.log('onAspectRadio called ' + JSON.stringify(data));
this.setState({ this.setState({
videoWidth: data.width, videoWidth: data.width,
videoHeight: data.height, videoHeight: data.height,
}) });
} };
onVideoBuffer = (param: any) => { onVideoBuffer = (param: any) => {
console.log('onVideoBuffer') console.log('onVideoBuffer');
this.setState({isLoading: param.isBuffering});
this.setState({ isLoading: param.isBuffering }) };
}
onReadyForDisplay = () => { onReadyForDisplay = () => {
console.log('onReadyForDisplay') console.log('onReadyForDisplay');
this.setState({isLoading: false});
this.setState({ isLoading: false }) };
}
onAudioBecomingNoisy = () => { onAudioBecomingNoisy = () => {
this.setState({ paused: true }) this.setState({paused: true});
}; };
onAudioFocusChanged = (event: {hasAudioFocus: boolean}) => { onAudioFocusChanged = (event: {hasAudioFocus: boolean}) => {
this.setState({ paused: !event.hasAudioFocus }) this.setState({paused: !event.hasAudioFocus});
}; };
getCurrentTimePercentage = () => { getCurrentTimePercentage = () => {
@ -199,80 +217,44 @@ class VideoPlayer extends Component {
return 0; return 0;
}; };
renderRateControl(rate: number) {
const isSelected = (this.state.rate === rate);
return (
<TouchableOpacity onPress={() => { this.setState({ rate }) }}>
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
{rate}
</Text>
</TouchableOpacity>
);
}
renderResizeModeControl(resizeMode: string) {
const isSelected = (this.state.resizeMode === resizeMode);
return (
<TouchableOpacity onPress={() => { this.setState({ resizeMode }) }}>
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
{resizeMode}
</Text>
</TouchableOpacity>
)
}
renderVolumeControl(volume: number) {
const isSelected = (this.state.volume === volume);
return (
<TouchableOpacity onPress={() => { this.setState({ volume }) }}>
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
{volume * 100}%
</Text>
</TouchableOpacity>
)
}
toast = (visible: boolean, message: string) => { toast = (visible: boolean, message: string) => {
if (visible) { if (visible) {
if (Platform.OS === 'android') {
ToastAndroid.showWithGravityAndOffset( ToastAndroid.showWithGravityAndOffset(
message, message,
ToastAndroid.LONG, ToastAndroid.LONG,
ToastAndroid.BOTTOM, ToastAndroid.BOTTOM,
25, 25,
50, 50,
) );
return null } else {
Alert.alert(message, message);
} }
return null
} }
onError = (err: any) => {
console.log(JSON.stringify(err?.error.errorCode))
this.toast(true, 'error: ' + err?.error.errorCode)
}
onEnd = () => {
this.channelUp()
}; };
onError = (err: any) => {
console.log(JSON.stringify(err?.error.errorCode));
this.toast(true, 'error: ' + err?.error.errorCode);
};
onEnd = () => {
this.channelUp();
};
toggleFullscreen() { toggleFullscreen() {
this.setState({ fullscreen: !this.state.fullscreen }) this.setState({fullscreen: !this.state.fullscreen});
} }
toggleControls() { toggleControls() {
this.setState({ showRNVControls: !this.state.showRNVControls }) this.setState({showRNVControls: !this.state.showRNVControls});
} }
toggleDecoration() { toggleDecoration() {
this.setState({ decoration: !this.state.decoration }) this.setState({decoration: !this.state.decoration});
if (this.state.decoration) { if (this.state.decoration) {
this.video.dismissFullscreenPlayer() this.video?.dismissFullscreenPlayer();
} else { } else {
this.video.presentFullscreenPlayer() this.video?.presentFullscreenPlayer();
} }
} }
@ -288,115 +270,23 @@ class VideoPlayer extends Component {
textTracks: [], textTracks: [],
selectedAudioTrack: undefined, selectedAudioTrack: undefined,
selectedTextTrack: undefined, selectedTextTrack: undefined,
}) });
} }
channelUp() { channelUp() {
console.log('channel up') console.log('channel up');
this.goToChannel((this.state.srcListId + 1) % this.srcList.length) this.goToChannel((this.state.srcListId + 1) % this.srcList.length);
} }
channelDown() { channelDown() {
console.log('channel down') console.log('channel down');
this.goToChannel((this.state.srcListId + this.srcList.length - 1) % this.srcList.length) this.goToChannel(
(this.state.srcListId + this.srcList.length - 1) % this.srcList.length,
);
} }
componentDidMount() { componentDidMount() {
this.initSeekPanResponder() this.initSeekPanResponder();
}
renderDecorationsControl() {
return (
<TouchableOpacity
onPress={() => {
this.toggleDecoration()
}}
>
<Text style={[styles.controlOption]}>{'decoration'}</Text>
</TouchableOpacity>
)
}
renderInfoControl() {
return (
<TouchableOpacity
onPress={() => {
this.popupInfo()
}}
>
<Text style={[styles.controlOption]}>{'decoderInfo'}</Text>
</TouchableOpacity>
)
}
renderFullScreenControl() {
return (
<TouchableOpacity
onPress={() => {
this.toggleFullscreen()
}}
>
<Text style={[styles.controlOption]}>{'fullscreen'}</Text>
</TouchableOpacity>
)
}
renderPause() {
return (
<TouchableOpacity
onPress={() => {
this.setState({ paused: !this.state.paused })
}}
>
<Text style={[styles.controlOption]}>
{this.state.paused ? 'pause' : 'playing'}
</Text>
</TouchableOpacity>
)
}
renderRepeatModeControl() {
return (
<TouchableOpacity
onPress={() => {
this.setState({ loop: !this.state.loop })
}}
>
<Text style={[styles.controlOption]}>
{this.state.loop ? 'loop enable' : 'loop disable'}
</Text>
</TouchableOpacity>
)
}
renderLeftControl() {
return (
<View>
<TouchableOpacity
onPress={() => {
this.channelDown()
}}
>
<Text style={[styles.leftRightControlOption]}>{'ChDown'}</Text>
</TouchableOpacity>
</View>
// onTimelineUpdated
)
}
renderRightControl() {
return (
<View>
<TouchableOpacity
onPress={() => {
this.channelUp()
}}
>
<Text style={[styles.leftRightControlOption]}>{'ChUp'}</Text>
</TouchableOpacity>
</View>
)
} }
/** /**
@ -413,11 +303,11 @@ class VideoPlayer extends Component {
*/ */
constrainToSeekerMinMax(val = 0) { constrainToSeekerMinMax(val = 0) {
if (val <= 0) { if (val <= 0) {
return 0 return 0;
} else if (val >= this.seekerWidth) { } else if (val >= this.seekerWidth) {
return this.seekerWidth return this.seekerWidth;
} }
return val return val;
} }
/** /**
@ -428,17 +318,17 @@ class VideoPlayer extends Component {
* @param {float} position position in px of seeker handle} * @param {float} position position in px of seeker handle}
*/ */
setSeekerPosition(position = 0) { setSeekerPosition(position = 0) {
const state = this.state const state = this.state;
position = this.constrainToSeekerMinMax(position) position = this.constrainToSeekerMinMax(position);
state.seekerFillWidth = position state.seekerFillWidth = position;
state.seekerPosition = position state.seekerPosition = position;
if (!state.seeking) { if (!state.seeking) {
state.seekerOffset = position state.seekerOffset = position;
} }
this.setState(state) this.setState(state);
} }
/** /**
@ -448,8 +338,8 @@ class VideoPlayer extends Component {
* @return {float} position of seeker handle in px based on currentTime * @return {float} position of seeker handle in px based on currentTime
*/ */
calculateSeekerPosition() { calculateSeekerPosition() {
const percent = this.state.currentTime / this.state.duration const percent = this.state.currentTime / this.state.duration;
return this.seekerWidth * percent return this.seekerWidth * percent;
} }
/** /**
@ -459,8 +349,8 @@ class VideoPlayer extends Component {
* @return {float} time in ms based on seekerPosition. * @return {float} time in ms based on seekerPosition.
*/ */
calculateTimeFromSeekerPosition() { calculateTimeFromSeekerPosition() {
const percent = this.state.seekerPosition / this.seekerWidth const percent = this.state.seekerPosition / this.seekerWidth;
return this.state.duration * percent return this.state.duration * percent;
} }
/** /**
@ -469,29 +359,29 @@ class VideoPlayer extends Component {
initSeekPanResponder() { initSeekPanResponder() {
this.seekPanResponder = PanResponder.create({ this.seekPanResponder = PanResponder.create({
// Ask to be the responder. // Ask to be the responder.
onStartShouldSetPanResponder: (evt, gestureState) => true, onStartShouldSetPanResponder: (_evt, _gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true, onMoveShouldSetPanResponder: (_evt, _gestureState) => true,
/** /**
* When we start the pan tell the machine that we're * When we start the pan tell the machine that we're
* seeking. This stops it from updating the seekbar * seeking. This stops it from updating the seekbar
* position in the onProgress listener. * position in the onProgress listener.
*/ */
onPanResponderGrant: (evt, gestureState) => { onPanResponderGrant: (evt, _gestureState) => {
const state = this.state const state = this.state;
// this.clearControlTimeout() // this.clearControlTimeout()
const position = evt.nativeEvent.locationX const position = evt.nativeEvent.locationX;
this.setSeekerPosition(position) this.setSeekerPosition(position);
state.seeking = true state.seeking = true;
this.setState(state) this.setState(state);
}, },
/** /**
* When panning, update the seekbar position, duh. * When panning, update the seekbar position, duh.
*/ */
onPanResponderMove: (evt, gestureState) => { onPanResponderMove: (evt, gestureState) => {
const position = this.state.seekerOffset + gestureState.dx const position = this.state.seekerOffset + gestureState.dx;
this.setSeekerPosition(position) this.setSeekerPosition(position);
}, },
/** /**
@ -499,140 +389,193 @@ class VideoPlayer extends Component {
* If you seek to the end of the video we fire the * If you seek to the end of the video we fire the
* onEnd callback * onEnd callback
*/ */
onPanResponderRelease: (evt, gestureState) => { onPanResponderRelease: (_evt, _gestureState) => {
const time = this.calculateTimeFromSeekerPosition() const time = this.calculateTimeFromSeekerPosition();
const state = this.state const state = this.state;
if (time >= state.duration && !state.isLoading) { if (time >= state.duration && !state.isLoading) {
state.paused = true state.paused = true;
this.onEnd() this.onEnd();
} else { } else {
this.video?.seek(time) this.video?.seek(time);
state.seeking = false state.seeking = false;
} }
this.setState(state) this.setState(state);
}, },
}) });
} }
renderSeekBar() { renderSeekBar() {
if (!this.seekPanResponder) { if (!this.seekPanResponder) {
return null return null;
} }
const seekerStyle = [
styles.seekbarFill,
{
width: this.state.seekerFillWidth > 0 ? this.state.seekerFillWidth : 0,
backgroundColor: '#FFF',
},
];
const seekerPositionStyle = [
styles.seekbarHandle,
{
left: this.state.seekerPosition > 0 ? this.state.seekerPosition : 0,
},
];
const seekerPointerStyle = [
styles.seekbarCircle,
{backgroundColor: '#FFF'},
];
return ( return (
<View <View
style={styles.seekbarContainer} style={styles.seekbarContainer}
{...this.seekPanResponder.panHandlers} {...this.seekPanResponder.panHandlers}
{...styles.generalControls} {...styles.generalControls}>
>
<View <View
style={styles.seekbarTrack} style={styles.seekbarTrack}
onLayout={(event) => (this.seekerWidth = event.nativeEvent.layout.width)} onLayout={event =>
pointerEvents={'none'} (this.seekerWidth = event.nativeEvent.layout.width)
> }
<View pointerEvents={'none'}>
style={[ <View style={seekerStyle} pointerEvents={'none'} />
styles.seekbarFill,
{
width:
this.state.seekerFillWidth > 0 ? this.state.seekerFillWidth : 0,
backgroundColor: '#FFF',
},
]}
pointerEvents={'none'}
/>
</View> </View>
<View <View style={seekerPositionStyle} pointerEvents={'none'}>
style={[ <View style={seekerPointerStyle} pointerEvents={'none'} />
styles.seekbarHandle,
{ left: this.state.seekerPosition > 0 ? this.state.seekerPosition : 0 },
]}
pointerEvents={'none'}
>
<View
style={[
styles.seekbarCircle,
{ backgroundColor: '#FFF' },
]}
pointerEvents={'none'}
/>
</View> </View>
</View> </View>
) );
} }
IndicatorLoadingView() { IndicatorLoadingView() {
if (this.state.isLoading) if (this.state.isLoading) {
return <ActivityIndicator color="#3235fd" size="large" style={styles.IndicatorStyle} /> return (
else return <View /> <ActivityIndicator
color="#3235fd"
size="large"
style={styles.IndicatorStyle}
/>
);
} else {
return <View />;
}
} }
renderTopControl() { renderTopControl() {
return (<> return (
<>
<Text style={[styles.controlOption]}> <Text style={[styles.controlOption]}>
{this.srcList[this.state.srcListId]?.description || 'local file'} {this.srcList[this.state.srcListId]?.description || 'local file'}
</Text> </Text>
<View> <View>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
this.toggleControls() this.toggleControls();
}} }}>
> <Text style={[styles.leftRightControlOption]}>
<Text style={[styles.leftRightControlOption]}>{this.state.showRNVControls ? 'Hide controls' : 'Show controls'}</Text> {this.state.showRNVControls ? 'Hide controls' : 'Show controls'}
</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</>) </>
);
} }
onRateSelected = (value: string | number) => {
this.setState({rate: value});
}
onVolumeSelected = (value: string | number) => {
this.setState({volume: value});
}
onResizeModeSelected = (value: string | number) => {
this.setState({resizeMode: value});
}
renderOverlay() { renderOverlay() {
return ( return (
<> <>
{this.IndicatorLoadingView()} {this.IndicatorLoadingView()}
<View style={styles.topControls}> <View style={styles.topControls}>
<View style={styles.resizeModeControl}>{this.renderTopControl()}</View> <View style={styles.resizeModeControl}>
{this.renderTopControl()}
</View>
</View> </View>
{!this.state.showRNVControls ? ( {!this.state.showRNVControls ? (
<> <>
<View style={styles.leftControls}> <View style={styles.leftControls}>
<View style={styles.resizeModeControl}>{this.renderLeftControl()}</View> <ToggleControl
</View><View style={styles.rightControls}> onPress={() => {
<View style={styles.resizeModeControl}>{this.renderRightControl()}</View> this.channelDown();
</View><View style={styles.bottomControls}> }}
text='ChDown'
/>
</View>
<View style={styles.rightControls}>
<ToggleControl
onPress={() => {
this.channelUp();
}}
text='ChUp'
/>
</View>
<View style={styles.bottomControls}>
<View style={styles.generalControls}> <View style={styles.generalControls}>
{Platform.OS === 'android' ? (
<View style={styles.generalControls}> <View style={styles.generalControls}>
<View style={styles.resizeModeControl}>{this.renderInfoControl()}</View> <ToggleControl
</View> onPress={() => {
<View style={styles.resizeModeControl}>{this.renderPause()}</View> this.popupInfo();
<View style={styles.resizeModeControl}> }}
{this.renderRepeatModeControl()} text='decoderInfo'
</View> />
<View style={styles.resizeModeControl}>
{this.renderFullScreenControl()}
</View>
<View style={styles.resizeModeControl}>
{this.renderDecorationsControl()}
</View> </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'
/>
</View> </View>
<View style={styles.generalControls}> <View style={styles.generalControls}>
<View style={styles.rateControl}> <MultiValueControl
{this.renderRateControl(0.25)} values={[0.25, 0.5, 1.0, 1.5, 2.0]}
{this.renderRateControl(0.5)} onPress={this.onRateSelected}
{this.renderRateControl(1.0)} selected={this.state.rate}
{this.renderRateControl(1.5)} />
{this.renderRateControl(2.0)} <MultiValueControl
</View> values={[0.5, 1, 1.5]}
onPress={this.onVolumeSelected}
<View style={styles.volumeControl}> selected={this.state.volume}
{this.renderVolumeControl(0.5)} />
{this.renderVolumeControl(1)} <MultiValueControl
{this.renderVolumeControl(1.5)} values={['cover', 'contain', 'stretch']}
</View> onPress={this.onResizeModeSelected}
selected={this.state.resizeMode}
<View style={styles.resizeModeControl}> />
{this.renderResizeModeControl('cover')}
{this.renderResizeModeControl('contain')}
{this.renderResizeModeControl('stretch')}
</View>
</View> </View>
{this.renderSeekBar()} {this.renderSeekBar()}
<View style={styles.generalControls}> <View style={styles.generalControls}>
@ -651,14 +594,14 @@ class VideoPlayer extends Component {
value: itemValue, value: itemValue,
}, },
}); });
}} }}>
> {this.state.audioTracks.map(track => {
{this.state.audioTracks.map((track) => {
return ( return (
<Picker.Item <Picker.Item
label={track.language} label={track.language}
value={track.language} value={track.language}
key={track.language} /> key={track.language}
/>
); );
})} })}
</Picker> </Picker>
@ -678,35 +621,38 @@ class VideoPlayer extends Component {
value: itemValue, value: itemValue,
}, },
}); });
}} }}>
>
<Picker.Item label={'none'} value={'none'} key={'none'} /> <Picker.Item label={'none'} value={'none'} key={'none'} />
{this.state.textTracks.map((track) => ( {this.state.textTracks.map(track => (
<Picker.Item <Picker.Item
label={track.language} label={track.language}
value={track.language} value={track.language}
key={track.language} /> key={track.language}
/>
))} ))}
</Picker> </Picker>
)} )}
</View> </View>
</View></> </View>
) : null
}
</> </>
) ) : null}
</>
);
} }
renderVideoView() { renderVideoView() {
const viewStyle = this.state.fullscreen ? styles.fullScreen : styles.halfScreen const viewStyle = this.state.fullscreen
? styles.fullScreen
: styles.halfScreen;
return ( return (
<TouchableOpacity style={viewStyle}> <TouchableOpacity style={viewStyle}>
<Video <Video
ref={(ref: Video) => { ref={(ref: Video) => {
this.video = ref this.video = ref;
}} }}
source={this.srcList[this.state.srcListId]} source={this.srcList[this.state.srcListId]}
//adTagUrl={this.srcList[this.state.srcListId]?.adTagUrl}
style={viewStyle} style={viewStyle}
rate={this.state.rate} rate={this.state.rate}
paused={this.state.paused} paused={this.state.paused}
@ -734,21 +680,21 @@ class VideoPlayer extends Component {
playInBackground={false} playInBackground={false}
/> />
</TouchableOpacity> </TouchableOpacity>
) );
} }
render() { render() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
{this.srcList[this.state.srcListId]?.noView ? null : this.renderVideoView()} {this.srcList[this.state.srcListId]?.noView
? null
: this.renderVideoView()}
{this.renderOverlay()} {this.renderOverlay()}
</View> </View>
) );
} }
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
@ -888,4 +834,4 @@ const styles = StyleSheet.create({
}, },
}); });
export default VideoPlayer export default VideoPlayer;