chore: rework examples (#4225)
* remove unused examples * init bare example with test app * add react-native-video * add test app suport in expo plugin * expo plugin: skip keys that are already in pod file * fix podfile * add src files * fix metro config * finalize react native test app configuration * init expo example * remove old examples * add guide for example * Add link to examples apps in docs * adopt bare example to CI tests * update CI workflows * CI build lib after node_modules install * fix examples readme * fix iOS CI * Add Example for DRM * Update examples/README.md * fix links * update examples README * sync example code * update README
This commit is contained in:
344
examples/expo/src/BasicExample.tsx
Normal file
344
examples/expo/src/BasicExample.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
import React, {type FC, useCallback, useRef, useState, useEffect} from 'react';
|
||||
|
||||
import {Platform, TouchableOpacity, View, StatusBar} from 'react-native';
|
||||
|
||||
import Video, {
|
||||
VideoRef,
|
||||
SelectedVideoTrackType,
|
||||
BufferingStrategyType,
|
||||
SelectedTrackType,
|
||||
ResizeMode,
|
||||
type AudioTrack,
|
||||
type OnAudioTracksData,
|
||||
type OnLoadData,
|
||||
type OnProgressData,
|
||||
type OnTextTracksData,
|
||||
type OnVideoAspectRatioData,
|
||||
type TextTrack,
|
||||
type OnBufferData,
|
||||
type OnAudioFocusChangedData,
|
||||
type OnVideoErrorData,
|
||||
type OnTextTrackDataChangedData,
|
||||
type OnSeekData,
|
||||
type OnPlaybackStateChangedData,
|
||||
type OnPlaybackRateChangeData,
|
||||
type OnVideoTracksData,
|
||||
type ReactVideoSource,
|
||||
type VideoTrack,
|
||||
type SelectedTrack,
|
||||
type SelectedVideoTrack,
|
||||
type EnumValues,
|
||||
OnBandwidthUpdateData,
|
||||
ControlsStyles,
|
||||
} from 'react-native-video';
|
||||
import styles from './styles';
|
||||
import {type AdditionalSourceInfo} from './types';
|
||||
import {
|
||||
bufferConfig,
|
||||
isAndroid,
|
||||
srcList,
|
||||
textTracksSelectionBy,
|
||||
audioTracksSelectionBy,
|
||||
} from './constants';
|
||||
import {Overlay, toast, VideoLoader} from './components';
|
||||
|
||||
type Props = NonNullable<unknown>;
|
||||
|
||||
const VideoPlayer: FC<Props> = ({}) => {
|
||||
const [rate, setRate] = useState(1);
|
||||
const [volume, setVolume] = useState(1);
|
||||
const [muted, setMuted] = useState(false);
|
||||
const [resizeMode, setResizeMode] = useState<EnumValues<ResizeMode>>(
|
||||
ResizeMode.CONTAIN,
|
||||
);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [_, setVideoSize] = useState({videoWidth: 0, videoHeight: 0});
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [fullscreen, setFullscreen] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [audioTracks, setAudioTracks] = useState<AudioTrack[]>([]);
|
||||
const [textTracks, setTextTracks] = useState<TextTrack[]>([]);
|
||||
const [videoTracks, setVideoTracks] = useState<VideoTrack[]>([]);
|
||||
const [selectedAudioTrack, setSelectedAudioTrack] = useState<
|
||||
SelectedTrack | undefined
|
||||
>(undefined);
|
||||
const [selectedTextTrack, setSelectedTextTrack] = useState<
|
||||
SelectedTrack | undefined
|
||||
>(undefined);
|
||||
const [selectedVideoTrack, setSelectedVideoTrack] =
|
||||
useState<SelectedVideoTrack>({
|
||||
type: SelectedVideoTrackType.AUTO,
|
||||
});
|
||||
const [srcListId, setSrcListId] = useState(0);
|
||||
const [repeat, setRepeat] = useState(false);
|
||||
const [controls, setControls] = useState(false);
|
||||
const [useCache, setUseCache] = useState(false);
|
||||
const [showPoster, setShowPoster] = useState<boolean>(false);
|
||||
const [showNotificationControls, setShowNotificationControls] =
|
||||
useState(false);
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
const viewStyle = fullscreen ? styles.fullScreen : styles.halfScreen;
|
||||
const currentSrc = srcList[srcListId];
|
||||
const additional = currentSrc as AdditionalSourceInfo;
|
||||
|
||||
const goToChannel = useCallback((channel: number) => {
|
||||
setSrcListId(channel);
|
||||
setDuration(0);
|
||||
setCurrentTime(0);
|
||||
setVideoSize({videoWidth: 0, videoHeight: 0});
|
||||
setIsLoading(false);
|
||||
setAudioTracks([]);
|
||||
setTextTracks([]);
|
||||
setSelectedAudioTrack(undefined);
|
||||
setSelectedTextTrack(undefined);
|
||||
setSelectedVideoTrack({
|
||||
type: SelectedVideoTrackType.AUTO,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const channelUp = useCallback(() => {
|
||||
console.log('channel up');
|
||||
goToChannel((srcListId + 1) % srcList.length);
|
||||
}, [goToChannel, srcListId]);
|
||||
|
||||
const channelDown = useCallback(() => {
|
||||
console.log('channel down');
|
||||
goToChannel((srcListId + srcList.length - 1) % srcList.length);
|
||||
}, [goToChannel, srcListId]);
|
||||
|
||||
const onAudioTracks = (data: OnAudioTracksData) => {
|
||||
console.log('onAudioTracks', data);
|
||||
const selectedTrack = data.audioTracks?.find((x: AudioTrack) => {
|
||||
return x.selected;
|
||||
});
|
||||
let value;
|
||||
if (audioTracksSelectionBy === SelectedTrackType.INDEX) {
|
||||
value = selectedTrack?.index;
|
||||
} else if (audioTracksSelectionBy === SelectedTrackType.LANGUAGE) {
|
||||
value = selectedTrack?.language;
|
||||
} else if (audioTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||
value = selectedTrack?.title;
|
||||
}
|
||||
setAudioTracks(data.audioTracks);
|
||||
setSelectedAudioTrack({
|
||||
type: audioTracksSelectionBy,
|
||||
value: value,
|
||||
});
|
||||
};
|
||||
|
||||
const onVideoTracks = (data: OnVideoTracksData) => {
|
||||
console.log('onVideoTracks', data.videoTracks);
|
||||
setVideoTracks(data.videoTracks);
|
||||
};
|
||||
|
||||
const onTextTracks = (data: OnTextTracksData) => {
|
||||
const selectedTrack = data.textTracks?.find((x: TextTrack) => {
|
||||
return x?.selected;
|
||||
});
|
||||
|
||||
setTextTracks(data.textTracks);
|
||||
let value;
|
||||
if (textTracksSelectionBy === SelectedTrackType.INDEX) {
|
||||
value = selectedTrack?.index;
|
||||
} else if (textTracksSelectionBy === SelectedTrackType.LANGUAGE) {
|
||||
value = selectedTrack?.language;
|
||||
} else if (textTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||
value = selectedTrack?.title;
|
||||
}
|
||||
setSelectedTextTrack({
|
||||
type: textTracksSelectionBy,
|
||||
value: value,
|
||||
});
|
||||
};
|
||||
|
||||
const onLoad = (data: OnLoadData) => {
|
||||
setDuration(data.duration);
|
||||
onAudioTracks(data);
|
||||
onTextTracks(data);
|
||||
onVideoTracks(data);
|
||||
};
|
||||
|
||||
const onProgress = (data: OnProgressData) => {
|
||||
setCurrentTime(data.currentTime);
|
||||
};
|
||||
|
||||
const onSeek = (data: OnSeekData) => {
|
||||
setCurrentTime(data.currentTime);
|
||||
setIsSeeking(false);
|
||||
};
|
||||
|
||||
const onVideoLoadStart = () => {
|
||||
console.log('onVideoLoadStart');
|
||||
setIsLoading(true);
|
||||
};
|
||||
|
||||
const onTextTrackDataChanged = (data: OnTextTrackDataChangedData) => {
|
||||
console.log(`Subtitles: ${JSON.stringify(data, null, 2)}`);
|
||||
};
|
||||
|
||||
const onAspectRatio = (data: OnVideoAspectRatioData) => {
|
||||
console.log('onAspectRadio called ' + JSON.stringify(data));
|
||||
setVideoSize({videoWidth: data.width, videoHeight: data.height});
|
||||
};
|
||||
|
||||
const onVideoBuffer = (param: OnBufferData) => {
|
||||
console.log('onVideoBuffer');
|
||||
setIsLoading(param.isBuffering);
|
||||
};
|
||||
|
||||
const onReadyForDisplay = () => {
|
||||
console.log('onReadyForDisplay');
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onAudioBecomingNoisy = () => {
|
||||
setPaused(true);
|
||||
};
|
||||
|
||||
const onAudioFocusChanged = (event: OnAudioFocusChangedData) => {
|
||||
setPaused(!event.hasAudioFocus);
|
||||
};
|
||||
|
||||
const onError = (err: OnVideoErrorData) => {
|
||||
console.log(JSON.stringify(err));
|
||||
toast(true, 'error: ' + JSON.stringify(err));
|
||||
};
|
||||
|
||||
const onEnd = () => {
|
||||
if (!repeat) {
|
||||
channelUp();
|
||||
}
|
||||
};
|
||||
|
||||
const onPlaybackRateChange = (data: OnPlaybackRateChangeData) => {
|
||||
console.log('onPlaybackRateChange', data);
|
||||
};
|
||||
|
||||
const onPlaybackStateChanged = (data: OnPlaybackStateChangedData) => {
|
||||
console.log('onPlaybackStateChanged', data);
|
||||
};
|
||||
|
||||
const onVideoBandwidthUpdate = (data: OnBandwidthUpdateData) => {
|
||||
console.log('onVideoBandwidthUpdate', data);
|
||||
};
|
||||
|
||||
const onFullScreenExit = () => {
|
||||
// iOS pauses video on exit from full screen
|
||||
Platform.OS === 'ios' && setPaused(true);
|
||||
};
|
||||
|
||||
const _renderLoader = showPoster ? () => <VideoLoader /> : undefined;
|
||||
|
||||
const _subtitleStyle = {subtitlesFollowVideo: true};
|
||||
const _controlsStyles: ControlsStyles = {
|
||||
hideNavigationBarOnFullScreenMode: true,
|
||||
hideNotificationBarOnFullScreenMode: true,
|
||||
liveLabel: 'LIVE',
|
||||
};
|
||||
const _bufferConfig = {
|
||||
...bufferConfig,
|
||||
cacheSizeMB: useCache ? 200 : 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar animated={true} backgroundColor="black" hidden={false} />
|
||||
|
||||
{(srcList[srcListId] as AdditionalSourceInfo)?.noView ? null : (
|
||||
<TouchableOpacity style={viewStyle}>
|
||||
<Video
|
||||
showNotificationControls={showNotificationControls}
|
||||
ref={videoRef}
|
||||
source={currentSrc as ReactVideoSource}
|
||||
adTagUrl={additional?.adTagUrl}
|
||||
drm={additional?.drm}
|
||||
style={viewStyle}
|
||||
rate={rate}
|
||||
paused={paused}
|
||||
volume={volume}
|
||||
muted={muted}
|
||||
controls={controls}
|
||||
resizeMode={resizeMode}
|
||||
onFullscreenPlayerWillDismiss={onFullScreenExit}
|
||||
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}
|
||||
onBandwidthUpdate={onVideoBandwidthUpdate}
|
||||
onSeek={onSeek}
|
||||
repeat={repeat}
|
||||
selectedTextTrack={selectedTextTrack}
|
||||
selectedAudioTrack={selectedAudioTrack}
|
||||
selectedVideoTrack={selectedVideoTrack}
|
||||
playInBackground={false}
|
||||
bufferConfig={_bufferConfig}
|
||||
preventsDisplaySleepDuringVideoPlayback={true}
|
||||
renderLoader={_renderLoader}
|
||||
onPlaybackRateChange={onPlaybackRateChange}
|
||||
onPlaybackStateChanged={onPlaybackStateChanged}
|
||||
bufferingStrategy={BufferingStrategyType.DEFAULT}
|
||||
debug={{enable: true, thread: true}}
|
||||
subtitleStyle={_subtitleStyle}
|
||||
controlsStyles={_controlsStyles}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Overlay
|
||||
channelDown={channelDown}
|
||||
channelUp={channelUp}
|
||||
ref={videoRef}
|
||||
videoTracks={videoTracks}
|
||||
selectedVideoTrack={selectedVideoTrack}
|
||||
setSelectedTextTrack={setSelectedTextTrack}
|
||||
audioTracks={audioTracks}
|
||||
controls={controls}
|
||||
resizeMode={resizeMode}
|
||||
textTracks={textTracks}
|
||||
selectedTextTrack={selectedTextTrack}
|
||||
selectedAudioTrack={selectedAudioTrack}
|
||||
setSelectedAudioTrack={setSelectedAudioTrack}
|
||||
setSelectedVideoTrack={setSelectedVideoTrack}
|
||||
currentTime={currentTime}
|
||||
setMuted={setMuted}
|
||||
muted={muted}
|
||||
duration={duration}
|
||||
paused={paused}
|
||||
volume={volume}
|
||||
setControls={setControls}
|
||||
showPoster={showPoster}
|
||||
rate={rate}
|
||||
setFullscreen={setFullscreen}
|
||||
setPaused={setPaused}
|
||||
isLoading={isLoading}
|
||||
isSeeking={isSeeking}
|
||||
setIsSeeking={setIsSeeking}
|
||||
repeat={repeat}
|
||||
setRepeat={setRepeat}
|
||||
setShowPoster={setShowPoster}
|
||||
setRate={setRate}
|
||||
setResizeMode={setResizeMode}
|
||||
setShowNotificationControls={setShowNotificationControls}
|
||||
showNotificationControls={showNotificationControls}
|
||||
setUseCache={setUseCache}
|
||||
setVolume={setVolume}
|
||||
useCache={useCache}
|
||||
srcListId={srcListId}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default VideoPlayer;
|
240
examples/expo/src/BasicExample.windows.tsx
Normal file
240
examples/expo/src/BasicExample.windows.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import Video, {ResizeMode} from 'react-native-video';
|
||||
|
||||
class VideoPlayer extends Component {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.onLoad = this.onLoad.bind(this);
|
||||
this.onProgress = this.onProgress.bind(this);
|
||||
}
|
||||
|
||||
state = {
|
||||
rate: 1,
|
||||
volume: 1,
|
||||
muted: false,
|
||||
resizeMode: ResizeMode.CONTAIN,
|
||||
duration: 0.0,
|
||||
currentTime: 0.0,
|
||||
paused: false,
|
||||
};
|
||||
|
||||
onLoad(data: any) {
|
||||
this.setState({duration: data.duration});
|
||||
}
|
||||
|
||||
onProgress(data: any) {
|
||||
this.setState({currentTime: data.currentTime});
|
||||
}
|
||||
|
||||
getCurrentTimePercentage() {
|
||||
if (this.state.currentTime > 0 && this.state.duration !== 0) {
|
||||
return this.state.currentTime / this.state.duration;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
renderRateControl(rate: number) {
|
||||
const isSelected = this.state.rate === rate;
|
||||
const style: TextStyle = StyleSheet.flatten([
|
||||
styles.controlOption,
|
||||
{fontWeight: isSelected ? 'bold' : 'normal'},
|
||||
]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
this.setState({rate: rate});
|
||||
}}>
|
||||
<Text style={style}>{rate}x</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
renderResizeModeControl(resizeMode: string) {
|
||||
const isSelected = this.state.resizeMode === resizeMode;
|
||||
const style: TextStyle = StyleSheet.flatten([
|
||||
styles.controlOption,
|
||||
{fontWeight: isSelected ? 'bold' : 'normal'},
|
||||
]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
this.setState({resizeMode: resizeMode});
|
||||
}}>
|
||||
<Text style={style}>{resizeMode}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
renderVolumeControl(volume: number) {
|
||||
const isSelected = this.state.volume === volume;
|
||||
const style: TextStyle = StyleSheet.flatten([
|
||||
styles.controlOption,
|
||||
{fontWeight: isSelected ? 'bold' : 'normal'},
|
||||
]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
this.setState({volume: volume});
|
||||
}}>
|
||||
<Text style={style}>{volume * 100}%</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
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
|
||||
source={require('./assets/videos/broadchurch.mp4')}
|
||||
style={styles.fullScreen}
|
||||
rate={this.state.rate}
|
||||
paused={this.state.paused}
|
||||
volume={this.state.volume}
|
||||
muted={this.state.muted}
|
||||
resizeMode={this.state.resizeMode}
|
||||
onLoad={this.onLoad}
|
||||
onProgress={this.onProgress}
|
||||
onEnd={() => {
|
||||
console.log('Done!');
|
||||
}}
|
||||
repeat={true}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.controls}>
|
||||
<View style={styles.generalControls}>
|
||||
<View style={styles.rateControl}>
|
||||
{this.renderRateControl(0.25)}
|
||||
{this.renderRateControl(0.5)}
|
||||
{this.renderRateControl(1.0)}
|
||||
{this.renderRateControl(1.5)}
|
||||
{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.trackingControls}>
|
||||
<View style={styles.progress}>
|
||||
<View
|
||||
style={[styles.innerProgressCompleted, {flex: flexCompleted}]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.innerProgressRemaining, {flex: flexRemaining}]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
progress: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
innerProgressCompleted: {
|
||||
height: 20,
|
||||
backgroundColor: '#cccccc',
|
||||
},
|
||||
innerProgressRemaining: {
|
||||
height: 20,
|
||||
backgroundColor: '#2C2C2C',
|
||||
},
|
||||
generalControls: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
rateControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
volumeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
resizeModeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
controlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
lineHeight: 12,
|
||||
},
|
||||
trackingControls: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
export default VideoPlayer;
|
1
examples/expo/src/assets/index.ts
Normal file
1
examples/expo/src/assets/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './videos';
|
BIN
examples/expo/src/assets/videos/broadchurch.mp4
Normal file
BIN
examples/expo/src/assets/videos/broadchurch.mp4
Normal file
Binary file not shown.
4
examples/expo/src/assets/videos/index.ts
Normal file
4
examples/expo/src/assets/videos/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const localeVideo = {
|
||||
broadchurch: require('./broadchurch.mp4'),
|
||||
portrait: require('./portrait.mp4'),
|
||||
};
|
BIN
examples/expo/src/assets/videos/portrait.mp4
Normal file
BIN
examples/expo/src/assets/videos/portrait.mp4
Normal file
Binary file not shown.
65
examples/expo/src/components/AudioTracksSelector.tsx
Normal file
65
examples/expo/src/components/AudioTracksSelector.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import {Picker} from '@react-native-picker/picker';
|
||||
import {Text} from 'react-native';
|
||||
import {
|
||||
SelectedTrackType,
|
||||
type AudioTrack,
|
||||
type SelectedTrack,
|
||||
} from 'react-native-video';
|
||||
import styles from '../styles';
|
||||
import React from 'react';
|
||||
|
||||
export interface AudioTrackSelectorType {
|
||||
audioTracks: Array<AudioTrack>;
|
||||
selectedAudioTrack: SelectedTrack | undefined;
|
||||
onValueChange: (arg0: string | number) => void;
|
||||
audioTracksSelectionBy: SelectedTrackType;
|
||||
}
|
||||
|
||||
export const AudioTrackSelector = ({
|
||||
audioTracks,
|
||||
selectedAudioTrack,
|
||||
onValueChange,
|
||||
audioTracksSelectionBy,
|
||||
}: AudioTrackSelectorType) => {
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.controlOption}>AudioTrack</Text>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={selectedAudioTrack?.value}
|
||||
onValueChange={itemValue => {
|
||||
if (itemValue !== 'empty') {
|
||||
console.log('on audio value change ' + itemValue);
|
||||
onValueChange(itemValue);
|
||||
}
|
||||
}}>
|
||||
{audioTracks?.length <= 0 ? (
|
||||
<Picker.Item label={'empty'} value={'empty'} key={'empty'} />
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{audioTracks.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
let value;
|
||||
if (audioTracksSelectionBy === SelectedTrackType.INDEX) {
|
||||
value = track.index;
|
||||
} else if (audioTracksSelectionBy === SelectedTrackType.LANGUAGE) {
|
||||
value = track.language;
|
||||
} else if (audioTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||
value = track.title;
|
||||
}
|
||||
return (
|
||||
<Picker.Item
|
||||
label={`${value} - ${track.selected}`}
|
||||
value={`${value}`}
|
||||
key={`${value}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
8
examples/expo/src/components/Indicator.tsx
Normal file
8
examples/expo/src/components/Indicator.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React, {memo} from 'react';
|
||||
import {ActivityIndicator} from 'react-native';
|
||||
|
||||
const _Indicator = () => {
|
||||
return <ActivityIndicator color="#3235fd" size="large" />;
|
||||
};
|
||||
|
||||
export const Indicator = memo(_Indicator);
|
74
examples/expo/src/components/MultiValueControl.tsx
Normal file
74
examples/expo/src/components/MultiValueControl.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {ResizeMode} from 'react-native-video';
|
||||
|
||||
/*
|
||||
* MultiValueControl displays a list clickable text view
|
||||
*/
|
||||
|
||||
interface MultiValueControlType<T> {
|
||||
// a list a string or number to be displayed
|
||||
values: Array<T>;
|
||||
// The selected value in values
|
||||
selected?: T;
|
||||
// callback to press onPress
|
||||
onPress: (arg: T) => void;
|
||||
}
|
||||
|
||||
export const MultiValueControl = <T extends number | string | ResizeMode>({
|
||||
values,
|
||||
selected,
|
||||
onPress,
|
||||
}: MultiValueControlType<T>) => {
|
||||
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 => {
|
||||
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;
|
350
examples/expo/src/components/Overlay.tsx
Normal file
350
examples/expo/src/components/Overlay.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from 'react';
|
||||
import {View} from 'react-native';
|
||||
import styles from '../styles.tsx';
|
||||
import {
|
||||
isAndroid,
|
||||
isIos,
|
||||
textTracksSelectionBy,
|
||||
audioTracksSelectionBy,
|
||||
} from '../constants';
|
||||
import {
|
||||
ResizeMode,
|
||||
VideoRef,
|
||||
SelectedTrackType,
|
||||
SelectedVideoTrackType,
|
||||
VideoDecoderProperties,
|
||||
type EnumValues,
|
||||
type TextTrack,
|
||||
type SelectedVideoTrack,
|
||||
type SelectedTrack,
|
||||
type VideoTrack,
|
||||
type AudioTrack,
|
||||
} from 'react-native-video';
|
||||
|
||||
import {toast} from './Toast';
|
||||
import {Seeker} from './Seeker';
|
||||
import {AudioTrackSelector} from './AudioTracksSelector';
|
||||
import {VideoTrackSelector} from './VideoTracksSelector';
|
||||
import {TextTrackSelector} from './TextTracksSelector';
|
||||
import {TopControl} from './TopControl';
|
||||
import {ToggleControl} from './ToggleControl';
|
||||
import {MultiValueControl} from './MultiValueControl';
|
||||
|
||||
type Props = {
|
||||
channelDown: () => void;
|
||||
channelUp: () => void;
|
||||
setFullscreen: Dispatch<SetStateAction<boolean>>;
|
||||
controls: boolean;
|
||||
setControls: Dispatch<SetStateAction<boolean>>;
|
||||
showNotificationControls: boolean;
|
||||
setShowNotificationControls: Dispatch<SetStateAction<boolean>>;
|
||||
selectedAudioTrack: SelectedTrack | undefined;
|
||||
setSelectedAudioTrack: Dispatch<SetStateAction<SelectedTrack | undefined>>;
|
||||
selectedTextTrack: SelectedTrack | undefined;
|
||||
setSelectedTextTrack: (value: SelectedTrack | undefined) => void;
|
||||
selectedVideoTrack: SelectedVideoTrack;
|
||||
setSelectedVideoTrack: (value: SelectedVideoTrack) => void;
|
||||
setIsSeeking: Dispatch<SetStateAction<boolean>>;
|
||||
rate: number;
|
||||
setRate: Dispatch<SetStateAction<number>>;
|
||||
volume: number;
|
||||
setVolume: (value: number) => void;
|
||||
resizeMode: EnumValues<ResizeMode>;
|
||||
setResizeMode: Dispatch<SetStateAction<EnumValues<ResizeMode>>>;
|
||||
isLoading: boolean;
|
||||
srcListId: number;
|
||||
useCache: boolean;
|
||||
setUseCache: Dispatch<SetStateAction<boolean>>;
|
||||
paused: boolean;
|
||||
setPaused: Dispatch<SetStateAction<boolean>>;
|
||||
repeat: boolean;
|
||||
setRepeat: Dispatch<SetStateAction<boolean>>;
|
||||
showPoster: boolean;
|
||||
setShowPoster: Dispatch<SetStateAction<boolean>>;
|
||||
muted: boolean;
|
||||
setMuted: Dispatch<SetStateAction<boolean>>;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
isSeeking: boolean;
|
||||
audioTracks: AudioTrack[];
|
||||
textTracks: TextTrack[];
|
||||
videoTracks: VideoTrack[];
|
||||
};
|
||||
|
||||
const _Overlay = forwardRef<VideoRef, Props>((props, ref) => {
|
||||
const {
|
||||
channelUp,
|
||||
channelDown,
|
||||
setFullscreen,
|
||||
setControls,
|
||||
controls,
|
||||
setShowNotificationControls,
|
||||
showNotificationControls,
|
||||
setSelectedAudioTrack,
|
||||
setSelectedTextTrack,
|
||||
setSelectedVideoTrack,
|
||||
setIsSeeking,
|
||||
rate,
|
||||
setRate,
|
||||
volume,
|
||||
setVolume,
|
||||
resizeMode,
|
||||
setResizeMode,
|
||||
isLoading,
|
||||
srcListId,
|
||||
setUseCache,
|
||||
useCache,
|
||||
paused,
|
||||
setPaused,
|
||||
setRepeat,
|
||||
repeat,
|
||||
setShowPoster,
|
||||
showPoster,
|
||||
setMuted,
|
||||
muted,
|
||||
duration,
|
||||
isSeeking,
|
||||
currentTime,
|
||||
textTracks,
|
||||
videoTracks,
|
||||
audioTracks,
|
||||
selectedAudioTrack,
|
||||
selectedVideoTrack,
|
||||
selectedTextTrack,
|
||||
} = 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 = () => {
|
||||
setFullscreen(prev => !prev);
|
||||
};
|
||||
const toggleControls = () => {
|
||||
setControls(prev => !prev);
|
||||
};
|
||||
|
||||
const openDecoration = () => {
|
||||
typeof ref !== 'function' && ref?.current?.setFullScreen(true);
|
||||
};
|
||||
|
||||
const toggleShowNotificationControls = () => {
|
||||
setShowNotificationControls(prev => !prev);
|
||||
};
|
||||
|
||||
const onSelectedAudioTrackChange = (itemValue: string | number) => {
|
||||
console.log('on audio value change ' + itemValue);
|
||||
if (itemValue === 'none') {
|
||||
setSelectedAudioTrack({
|
||||
type: SelectedTrackType.DISABLED,
|
||||
});
|
||||
} else {
|
||||
setSelectedAudioTrack({type: audioTracksSelectionBy, value: itemValue});
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectedTextTrackChange = (itemValue: string) => {
|
||||
console.log('on value change ' + itemValue);
|
||||
setSelectedTextTrack({type: textTracksSelectionBy, value: itemValue});
|
||||
};
|
||||
|
||||
const onSelectedVideoTrackChange = (itemValue: string) => {
|
||||
console.log('on value change ' + itemValue);
|
||||
if (itemValue === undefined || itemValue === 'auto') {
|
||||
setSelectedVideoTrack({
|
||||
type: SelectedVideoTrackType.AUTO,
|
||||
});
|
||||
} else {
|
||||
setSelectedVideoTrack({
|
||||
type: SelectedVideoTrackType.INDEX,
|
||||
value: itemValue,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const videoSeek = (position: number) => {
|
||||
setIsSeeking(true);
|
||||
typeof ref !== 'function' && ref?.current?.seek(position);
|
||||
};
|
||||
|
||||
const onRateSelected = (value: number) => {
|
||||
setRate(value);
|
||||
};
|
||||
|
||||
const onVolumeSelected = (value: number) => {
|
||||
setVolume(value);
|
||||
};
|
||||
|
||||
const onResizeModeSelected = (value: EnumValues<ResizeMode>) => {
|
||||
setResizeMode(value);
|
||||
};
|
||||
|
||||
const toggleCache = () => setUseCache(prev => !prev);
|
||||
|
||||
const togglePause = () => setPaused(prev => !prev);
|
||||
|
||||
const toggleRepeat = () => setRepeat(prev => !prev);
|
||||
|
||||
const togglePoster = () => setShowPoster(prev => !prev);
|
||||
|
||||
const toggleMuted = () => setMuted(prev => !prev);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.topControls}>
|
||||
<View style={styles.resizeModeControl}>
|
||||
<TopControl
|
||||
srcListId={srcListId}
|
||||
showRNVControls={controls}
|
||||
toggleControls={toggleControls}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{!controls ? (
|
||||
<>
|
||||
<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={useCache}
|
||||
onPress={toggleCache}
|
||||
selectedText="enable cache"
|
||||
unselectedText="disable cache"
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
<ToggleControl
|
||||
isSelected={paused}
|
||||
onPress={togglePause}
|
||||
selectedText="pause"
|
||||
unselectedText="playing"
|
||||
/>
|
||||
<ToggleControl
|
||||
isSelected={repeat}
|
||||
onPress={toggleRepeat}
|
||||
selectedText="loop enable"
|
||||
unselectedText="loop disable"
|
||||
/>
|
||||
<ToggleControl onPress={toggleFullscreen} text="fullscreen" />
|
||||
<ToggleControl onPress={openDecoration} text="decoration" />
|
||||
<ToggleControl
|
||||
isSelected={showPoster}
|
||||
onPress={togglePoster}
|
||||
selectedText="poster"
|
||||
unselectedText="no poster"
|
||||
/>
|
||||
<ToggleControl
|
||||
isSelected={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={rate}
|
||||
/>
|
||||
{/* shall be replaced by slider */}
|
||||
<MultiValueControl
|
||||
values={[0.5, 1, 1.5]}
|
||||
onPress={onVolumeSelected}
|
||||
selected={volume}
|
||||
/>
|
||||
<MultiValueControl
|
||||
values={[
|
||||
ResizeMode.COVER,
|
||||
ResizeMode.CONTAIN,
|
||||
ResizeMode.STRETCH,
|
||||
]}
|
||||
onPress={onResizeModeSelected}
|
||||
selected={resizeMode}
|
||||
/>
|
||||
<ToggleControl
|
||||
isSelected={muted}
|
||||
onPress={toggleMuted}
|
||||
text="muted"
|
||||
/>
|
||||
{isIos ? (
|
||||
<ToggleControl
|
||||
isSelected={paused}
|
||||
onPress={() => {
|
||||
typeof ref !== 'function' &&
|
||||
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={currentTime}
|
||||
duration={duration}
|
||||
isLoading={isLoading}
|
||||
videoSeek={prop => videoSeek(prop)}
|
||||
isUISeeking={isSeeking}
|
||||
/>
|
||||
<View style={styles.generalControls}>
|
||||
<AudioTrackSelector
|
||||
audioTracks={audioTracks}
|
||||
selectedAudioTrack={selectedAudioTrack}
|
||||
onValueChange={onSelectedAudioTrackChange}
|
||||
audioTracksSelectionBy={audioTracksSelectionBy}
|
||||
/>
|
||||
<TextTrackSelector
|
||||
textTracks={textTracks}
|
||||
selectedTextTrack={selectedTextTrack}
|
||||
onValueChange={onSelectedTextTrackChange}
|
||||
textTracksSelectionBy={textTracksSelectionBy}
|
||||
/>
|
||||
<VideoTrackSelector
|
||||
videoTracks={videoTracks}
|
||||
selectedVideoTrack={selectedVideoTrack}
|
||||
onValueChange={onSelectedVideoTrackChange}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const Overlay = memo(_Overlay);
|
152
examples/expo/src/components/Seeker.tsx
Normal file
152
examples/expo/src/components/Seeker.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {PanResponder, View} from 'react-native';
|
||||
import styles from '../styles';
|
||||
|
||||
interface SeekerProps {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
isLoading: boolean;
|
||||
isUISeeking: boolean;
|
||||
videoSeek: (arg0: number) => void;
|
||||
}
|
||||
|
||||
export const Seeker = ({
|
||||
currentTime,
|
||||
duration,
|
||||
isLoading,
|
||||
isUISeeking,
|
||||
videoSeek,
|
||||
}: SeekerProps) => {
|
||||
const [seeking, setSeeking] = useState(false);
|
||||
const [seekerPosition, setSeekerPosition] = useState(0);
|
||||
const [seekerWidth, setSeekerWidth] = useState(0);
|
||||
|
||||
/**
|
||||
* Set the position of the seekbar's components
|
||||
* (both fill and handle) according to the
|
||||
* position supplied.
|
||||
*
|
||||
* @param {float} position position in px of seeker handle}
|
||||
*/
|
||||
const updateSeekerPosition = useCallback(
|
||||
(position = 0) => {
|
||||
if (position <= 0) {
|
||||
position = 0;
|
||||
} else if (position >= seekerWidth) {
|
||||
position = seekerWidth;
|
||||
}
|
||||
setSeekerPosition(position);
|
||||
},
|
||||
[seekerWidth],
|
||||
);
|
||||
|
||||
/**
|
||||
* Return the time that the video should be at
|
||||
* based on where the seeker handle is.
|
||||
*
|
||||
* @return {float} time in ms based on seekerPosition.
|
||||
*/
|
||||
const calculateTimeFromSeekerPosition = () => {
|
||||
const percent = seekerPosition / seekerWidth;
|
||||
return duration * percent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get our seekbar responder going
|
||||
*/
|
||||
|
||||
const seekPanResponder = PanResponder.create({
|
||||
// Ask to be the responder.
|
||||
onStartShouldSetPanResponder: (_evt, _gestureState) => true,
|
||||
onMoveShouldSetPanResponder: (_evt, _gestureState) => true,
|
||||
|
||||
/**
|
||||
* When we start the pan tell the machine that we're
|
||||
* seeking. This stops it from updating the seekbar
|
||||
* position in the onProgress listener.
|
||||
*/
|
||||
onPanResponderGrant: (evt, _gestureState) => {
|
||||
const position = evt.nativeEvent.locationX;
|
||||
updateSeekerPosition(position);
|
||||
setSeeking(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* When panning, update the seekbar position, duh.
|
||||
*/
|
||||
onPanResponderMove: (evt, _gestureState) => {
|
||||
const position = evt.nativeEvent.locationX;
|
||||
updateSeekerPosition(position);
|
||||
},
|
||||
|
||||
/**
|
||||
* On release we update the time and seek to it in the video.
|
||||
* If you seek to the end of the video we fire the
|
||||
* onEnd callback
|
||||
*/
|
||||
onPanResponderRelease: (_evt, _gestureState) => {
|
||||
const time = calculateTimeFromSeekerPosition();
|
||||
if (time >= duration && !isLoading) {
|
||||
// FIXME ...
|
||||
// state.paused = true;
|
||||
// this.onEnd();
|
||||
} else {
|
||||
videoSeek(time);
|
||||
setSeeking(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !seeking && !isUISeeking) {
|
||||
const percent = currentTime / duration;
|
||||
const position = seekerWidth * percent;
|
||||
updateSeekerPosition(position);
|
||||
}
|
||||
}, [
|
||||
currentTime,
|
||||
duration,
|
||||
isLoading,
|
||||
seekerWidth,
|
||||
seeking,
|
||||
isUISeeking,
|
||||
updateSeekerPosition,
|
||||
]);
|
||||
|
||||
if (!seekPanResponder) {
|
||||
return null;
|
||||
}
|
||||
const seekerStyle = [
|
||||
styles.seekbarFill,
|
||||
{
|
||||
width: seekerPosition > 0 ? seekerPosition : 0,
|
||||
backgroundColor: '#FFF',
|
||||
},
|
||||
];
|
||||
|
||||
const seekerPositionStyle = [
|
||||
styles.seekbarHandle,
|
||||
{
|
||||
left: seekerPosition > 0 ? seekerPosition : 0,
|
||||
},
|
||||
];
|
||||
|
||||
const seekerPointerStyle = [styles.seekbarCircle, {backgroundColor: '#FFF'}];
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.seekbarContainer}
|
||||
{...seekPanResponder.panHandlers}
|
||||
{...styles.generalControls}>
|
||||
<View
|
||||
style={styles.seekbarTrack}
|
||||
onLayout={event => setSeekerWidth(event.nativeEvent.layout.width)}
|
||||
pointerEvents={'none'}>
|
||||
<View style={seekerStyle} pointerEvents={'none'} />
|
||||
</View>
|
||||
<View style={seekerPositionStyle} pointerEvents={'none'}>
|
||||
<View style={seekerPointerStyle} pointerEvents={'none'} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
58
examples/expo/src/components/TextTracksSelector.tsx
Normal file
58
examples/expo/src/components/TextTracksSelector.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import {Picker} from '@react-native-picker/picker';
|
||||
import {Text} from 'react-native';
|
||||
import {
|
||||
type TextTrack,
|
||||
type SelectedTrack,
|
||||
SelectedTrackType,
|
||||
} from 'react-native-video';
|
||||
import styles from '../styles';
|
||||
import React from 'react';
|
||||
|
||||
export interface TextTrackSelectorType {
|
||||
textTracks: Array<TextTrack>;
|
||||
selectedTextTrack: SelectedTrack | undefined;
|
||||
onValueChange: (arg0: string) => void;
|
||||
textTracksSelectionBy: string;
|
||||
}
|
||||
|
||||
export const TextTrackSelector = ({
|
||||
textTracks,
|
||||
selectedTextTrack,
|
||||
onValueChange,
|
||||
textTracksSelectionBy,
|
||||
}: TextTrackSelectorType) => {
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.controlOption}>TextTrack</Text>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={`${selectedTextTrack?.value}`}
|
||||
onValueChange={itemValue => {
|
||||
if (itemValue !== 'empty') {
|
||||
onValueChange(itemValue);
|
||||
}
|
||||
}}>
|
||||
{textTracks?.length <= 0 ? (
|
||||
<Picker.Item label={'empty'} value={'empty'} key={'empty'} />
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{textTracks.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
let value;
|
||||
if (textTracksSelectionBy === SelectedTrackType.INDEX) {
|
||||
value = track.index;
|
||||
} else if (textTracksSelectionBy === SelectedTrackType.LANGUAGE) {
|
||||
value = track.language;
|
||||
} else if (textTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||
value = track.title;
|
||||
}
|
||||
return <Picker.Item label={`${value}`} value={value} key={value} />;
|
||||
})}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
18
examples/expo/src/components/Toast.ts
Normal file
18
examples/expo/src/components/Toast.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
73
examples/expo/src/components/ToggleControl.tsx
Normal file
73
examples/expo/src/components/ToggleControl.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
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: () => void;
|
||||
}
|
||||
|
||||
export 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;
|
37
examples/expo/src/components/TopControl.tsx
Normal file
37
examples/expo/src/components/TopControl.tsx
Normal 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 {type 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);
|
15
examples/expo/src/components/VideoLoader.tsx
Normal file
15
examples/expo/src/components/VideoLoader.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import {Text, View} from 'react-native';
|
||||
import {Indicator} from './Indicator.tsx';
|
||||
import React, {memo} from 'react';
|
||||
import styles from '../styles.tsx';
|
||||
|
||||
const _VideoLoader = () => {
|
||||
return (
|
||||
<View style={styles.indicatorContainer}>
|
||||
<Text style={styles.indicatorText}>Loading...</Text>
|
||||
<Indicator />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const VideoLoader = memo(_VideoLoader);
|
62
examples/expo/src/components/VideoTracksSelector.tsx
Normal file
62
examples/expo/src/components/VideoTracksSelector.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import {Picker} from '@react-native-picker/picker';
|
||||
import {Text} from 'react-native';
|
||||
import {
|
||||
SelectedVideoTrackType,
|
||||
type SelectedVideoTrack,
|
||||
type VideoTrack,
|
||||
} from 'react-native-video';
|
||||
import styles from '../styles';
|
||||
import React from 'react';
|
||||
|
||||
export interface VideoTrackSelectorType {
|
||||
videoTracks: Array<VideoTrack>;
|
||||
selectedVideoTrack: SelectedVideoTrack | undefined;
|
||||
onValueChange: (arg0: string) => void;
|
||||
}
|
||||
|
||||
export const VideoTrackSelector = ({
|
||||
videoTracks,
|
||||
selectedVideoTrack,
|
||||
onValueChange,
|
||||
}: VideoTrackSelectorType) => {
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.controlOption}>VideoTrack</Text>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={
|
||||
selectedVideoTrack === undefined ||
|
||||
selectedVideoTrack?.type === SelectedVideoTrackType.AUTO
|
||||
? 'auto'
|
||||
: `${selectedVideoTrack?.value}`
|
||||
}
|
||||
onValueChange={itemValue => {
|
||||
if (itemValue !== 'empty') {
|
||||
onValueChange(itemValue);
|
||||
}
|
||||
}}>
|
||||
<Picker.Item label={'auto'} value={'auto'} key={'auto'} />
|
||||
{videoTracks?.length <= 0 || videoTracks?.length <= 0 ? (
|
||||
<Picker.Item label={'empty'} value={'empty'} key={'empty'} />
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{videoTracks?.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Picker.Item
|
||||
label={`${track.width}x${track.height} ${Math.floor(
|
||||
(track.bitrate || 0) / 8 / 1024,
|
||||
)} Kbps`}
|
||||
value={`${track.index}`}
|
||||
key={track.index}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
11
examples/expo/src/components/index.ts
Normal file
11
examples/expo/src/components/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from './VideoLoader';
|
||||
export * from './Indicator';
|
||||
export * from './Seeker';
|
||||
export * from './AudioTracksSelector';
|
||||
export * from './VideoTracksSelector';
|
||||
export * from './TextTracksSelector';
|
||||
export * from './Overlay';
|
||||
export * from './TopControl';
|
||||
export * from './Toast';
|
||||
export * from './ToggleControl';
|
||||
export * from './MultiValueControl';
|
182
examples/expo/src/constants/general.ts
Normal file
182
examples/expo/src/constants/general.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
BufferConfig,
|
||||
DRMType,
|
||||
ISO639_1,
|
||||
SelectedTrackType,
|
||||
TextTrackType,
|
||||
} from 'react-native-video';
|
||||
import {SampleVideoSource} from '../types';
|
||||
import {localeVideo} from '../assets';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
// This constant allows to change how the sample behaves regarding to audio and texts selection.
|
||||
// You can change it to change how selector will use tracks information.
|
||||
// by default, index will be displayed and index will be applied to selected tracks.
|
||||
// You can also use LANGUAGE or TITLE
|
||||
export const textTracksSelectionBy = SelectedTrackType.INDEX;
|
||||
export const audioTracksSelectionBy = SelectedTrackType.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: 'video with 90° rotation',
|
||||
uri: 'https://bn-dev.fra1.digitaloceanspaces.com/km-tournament/uploads/rn_image_picker_lib_temp_2ee86a27_9312_4548_84af_7fd75d9ad4dd_ad8b20587a.mp4',
|
||||
},
|
||||
{
|
||||
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: 'mp3 with texttrack',
|
||||
uri: 'https://traffic.libsyn.com/democracynow/wx2024-0702_SOT_DeadCalm-LucileSmith-FULL-V2.mxf-audio.mp3', // an mp3 file
|
||||
textTracks: [], // empty text track list
|
||||
},
|
||||
{
|
||||
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: SampleVideoSource[] = [];
|
||||
|
||||
export const srcAndroidList: SampleVideoSource[] = [
|
||||
{
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
const platformSrc: SampleVideoSource[] = isAndroid
|
||||
? srcAndroidList
|
||||
: srcIosList;
|
||||
|
||||
export const srcList: SampleVideoSource[] =
|
||||
platformSrc.concat(srcAllPlatformList);
|
||||
|
||||
export const bufferConfig: BufferConfig = {
|
||||
minBufferMs: 15000,
|
||||
maxBufferMs: 50000,
|
||||
bufferForPlaybackMs: 2500,
|
||||
bufferForPlaybackAfterRebufferMs: 5000,
|
||||
live: {
|
||||
targetOffsetMs: 500,
|
||||
},
|
||||
};
|
1
examples/expo/src/constants/index.ts
Normal file
1
examples/expo/src/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './general';
|
173
examples/expo/src/styles.tsx
Normal file
173
examples/expo/src/styles.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
halfScreen: {
|
||||
position: 'absolute',
|
||||
top: 50,
|
||||
left: 50,
|
||||
bottom: 100,
|
||||
right: 100,
|
||||
},
|
||||
fullScreen: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
bottomControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
leftControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
},
|
||||
rightControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
},
|
||||
topControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 4,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
generalControls: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
rateControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
volumeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
resizeModeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
leftRightControlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
padding: 10,
|
||||
lineHeight: 12,
|
||||
},
|
||||
controlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
lineHeight: 12,
|
||||
},
|
||||
pickerContainer: {
|
||||
width: 100,
|
||||
alignSelf: 'center',
|
||||
color: 'white',
|
||||
borderWidth: 1,
|
||||
borderColor: 'red',
|
||||
},
|
||||
indicatorContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
indicatorText: {
|
||||
color: 'white',
|
||||
},
|
||||
seekbarContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
height: 30,
|
||||
},
|
||||
seekbarTrack: {
|
||||
backgroundColor: '#333',
|
||||
height: 1,
|
||||
position: 'relative',
|
||||
top: 14,
|
||||
width: '100%',
|
||||
},
|
||||
seekbarFill: {
|
||||
backgroundColor: '#FFF',
|
||||
height: 1,
|
||||
width: '100%',
|
||||
},
|
||||
seekbarHandle: {
|
||||
position: 'absolute',
|
||||
marginLeft: -7,
|
||||
height: 28,
|
||||
width: 28,
|
||||
},
|
||||
seekbarCircle: {
|
||||
borderRadius: 12,
|
||||
position: 'relative',
|
||||
top: 8,
|
||||
left: 8,
|
||||
height: 12,
|
||||
width: 12,
|
||||
},
|
||||
picker: {
|
||||
flex: 1,
|
||||
color: 'white',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
pickerItem: {
|
||||
color: 'white',
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
emptyPickerItem: {
|
||||
color: 'white',
|
||||
marginTop: 20,
|
||||
marginLeft: 20,
|
||||
flex: 1,
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
topControlsContainer: {
|
||||
paddingTop: 30,
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
1
examples/expo/src/types/index.ts
Normal file
1
examples/expo/src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './types';
|
11
examples/expo/src/types/types.ts
Normal file
11
examples/expo/src/types/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {Drm, ReactVideoSource, TextTracks} from 'react-native-video';
|
||||
|
||||
export type AdditionalSourceInfo = {
|
||||
textTracks?: TextTracks;
|
||||
adTagUrl?: string;
|
||||
description?: string;
|
||||
drm?: Drm;
|
||||
noView?: boolean;
|
||||
};
|
||||
|
||||
export type SampleVideoSource = ReactVideoSource | AdditionalSourceInfo;
|
Reference in New Issue
Block a user