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:
Krzysztof Moch
2024-10-20 20:04:02 +02:00
committed by GitHub
parent 7c7d83b6e5
commit 9eb5502076
445 changed files with 5510 additions and 106905 deletions

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

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

View File

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

Binary file not shown.

View File

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

Binary file not shown.

View 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>
</>
);
};

View 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);

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

View 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);

View 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>
);
};

View 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>
</>
);
};

View File

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

View File

@@ -0,0 +1,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;

View File

@@ -0,0 +1,37 @@
import React, {FC, memo} from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
import styles from '../styles.tsx';
import {srcList} from '../constants';
import {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);

View 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);

View 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>
</>
);
};

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

View 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,
},
};

View File

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

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

View File

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

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