react-native-video-enhanced
Version:
enhanced version of react-native-video to support more interactive video controls on Android and iOS
1,413 lines (1,275 loc) • 41.1 kB
JavaScript
import React, { Component } from 'react';
import Video from 'react-native-video';
import {
TouchableWithoutFeedback,
TouchableHighlight,
ImageBackground,
PanResponder,
StyleSheet,
Animated,
SafeAreaView,
Easing,
Image,
View,
TouchableOpacity,
Text
} from 'react-native';
import Slider from "@react-native-community/slider";
import _ from 'lodash';
import { ProgressBar } from './ProgressBar';
let currentSeekTime = 0;
export default class VideoPlayer extends Component {
static defaultProps = {
toggleResizeModeOnFullscreen: true,
playInBackground: false,
playWhenInactive: false,
showOnStart: true,
resizeMode: 'contain',
paused: false,
repeat: false,
volume: 1,
muted: false,
title: '',
rate: 1,
isFullscreen: false,
};
constructor( props ) {
super( props );
/**
* All of our values that are updated by the
* methods and listeners in this class
*/
this.state = {
// Video
resizeMode: this.props.resizeMode,
paused: this.props.paused,
muted: this.props.muted,
volume: this.props.volume,
rate: this.props.rate,
// Controls
isFullscreen: this.props.isFullScreen || this.props.resizeMode === 'cover' || false,
showTimeRemaining: true,
volumeTrackWidth: 0,
lastScreenPress: 0,
volumeFillWidth: 0,
seekerFillWidth: 0,
showControls: this.props.showOnStart,
volumePosition: 0,
seekerPosition: 0,
volumeOffset: 0,
seekerOffset: 0,
seeking: false,
loading: false,
currentTime: 0,
error: false,
duration: 0,
source: this.props.source,
title: this.props.title || '',
orientation: this.props.orientation
};
/**
* Any options that can be set at init.
*/
this.opts = {
playWhenInactive: this.props.playWhenInactive,
playInBackground: this.props.playInBackground,
repeat: this.props.repeat,
title: this.state.title,
};
/**
* Our app listeners and associated methods
*/
this.events = {
onError: this.props.onError || this._onError.bind( this ),
onBack: this.props.onBack || this._onBack.bind( this ),
onEnd: this.props.onEnd || this._onEnd.bind( this ),
onScreenTouch: this._onScreenTouch.bind( this ),
onEnterFullscreen: this.props.onEnterFullscreen,
onExitFullscreen: this.props.onExitFullscreen,
onLoadStart: this._onLoadStart.bind( this ),
onProgress: this._onProgress.bind( this ),
onLoad: this._onLoad.bind( this ),
onPause: this.props.onPause,
onPlay: this.props.onPlay,
onForward: this._onForward,
onRewind: this._onRewind
};
/**
* Functions used throughout the application
*/
this.methods = {
toggleFullscreen: this._toggleFullscreen.bind( this ),
togglePlayPause: this._togglePlayPause.bind( this ),
toggleControls: this._toggleControls.bind( this ),
toggleTimer: this._toggleTimer.bind( this ),
};
/**
* Player information
*/
this.player = {
controlTimeoutDelay: this.props.controlTimeout || 15000,
volumePanResponder: PanResponder,
seekPanResponder: PanResponder,
controlTimeout: null,
volumeWidth: 150,
iconOffset: 0,
seekerWidth: 0,
ref: Video,
};
/**
* Various animations
*/
const initialValue = this.props.showOnStart ? 1 : 0;
this.animations = {
bottomControl: {
marginBottom: new Animated.Value( 0 ),
opacity: new Animated.Value( initialValue ),
},
topControl: {
marginTop: new Animated.Value( 0 ),
opacity: new Animated.Value( initialValue ),
},
video: {
opacity: new Animated.Value( 1 ),
},
loader: {
rotate: new Animated.Value( 0 ),
MAX_VALUE: 360,
}
};
/**
* Various styles that be added...
*/
this.styles = {
videoStyle: this.props.videoStyle || {},
containerStyle: this.props.style || {}
};
}
/**
| -------------------------------------------------------
| Events
| -------------------------------------------------------
|
| These are the events that the <Video> component uses
| and can be overridden by assigning it as a prop.
| It is suggested that you override onEnd.
|
*/
/**
* When load starts we display a loading icon
* and show the controls.
*/
_onLoadStart() {
let state = this.state;
state.loading = true;
this.loadAnimation();
this.setState( state );
if ( typeof this.props.onLoadStart === 'function' ) {
this.props.onLoadStart(...arguments);
}
}
/**
* When load is finished we hide the load icon
* and hide the controls. We also set the
* video duration.
*
* @param {object} data The video meta data
*/
_onLoad( data = {} ) {
let state = this.state;
state.duration = data.duration;
state.loading = false;
this.setState( state );
if ( state.showControls ) {
this.setControlTimeout();
}
if ( typeof this.props.onLoad === 'function' ) {
this.props.onLoad(...arguments);
}
}
_onForward = () => {
const position = this.state.currentTime + 15
this.seekTo(position)
}
_onRewind = () => {
const position = this.state.currentTime - 15
this.seekTo(position)
}
/**
* For onprogress we fire listeners that
* update our seekbar and timer.
*
* @param {object} data The video meta data
*/
_onProgress( data = {} ) {
let state = this.state;
state.currentTime = data.currentTime;
if ( ! state.seeking ) {
const position = this.calculateSeekerPosition();
this.setSeekerPosition( position );
}
if ( typeof this.props.onProgress === 'function' ) {
this.props.onProgress(...arguments);
}
this.setState( state );
}
/**
* It is suggested that you override this
* command so your app knows what to do.
* Either close the video or go to a
* new page.
*/
_onEnd() {}
/**
* Set the error state to true which then
* changes our renderError function
*
* @param {object} err Err obj returned from <Video> component
*/
_onError( err ) {
let state = this.state;
state.error = true;
state.loading = false;
this.setState( state );
}
/**
* This is a single and double tap listener
* when the user taps the screen anywhere.
* One tap toggles controls, two toggles
* fullscreen mode.
*/
_onScreenTouch() {
let state = this.state;
const time = new Date().getTime();
const delta = time - state.lastScreenPress;
if ( delta < 300 ) {
this.methods.toggleFullscreen();
}
this.methods.toggleControls();
state.lastScreenPress = time;
this.setState( state );
}
/**
| -------------------------------------------------------
| Methods
| -------------------------------------------------------
|
| These are all of our functions that interact with
| various parts of the class. Anything from
| calculating time remaining in a video
| to handling control operations.
|
*/
/**
* Set a timeout when the controls are shown
* that hides them after a length of time.
* Default is 15s
*/
setControlTimeout() {
this.player.controlTimeout = setTimeout( ()=> {
this._hideControls();
}, this.player.controlTimeoutDelay );
}
/**
* Clear the hide controls timeout.
*/
clearControlTimeout() {
clearTimeout( this.player.controlTimeout );
}
/**
* Reset the timer completely
*/
resetControlTimeout() {
this.clearControlTimeout();
this.setControlTimeout();
}
/**
* Animation to hide controls. We fade the
* display to 0 then move them off the
* screen so they're not interactable
*/
hideControlAnimation() {
Animated.parallel([
Animated.timing(
this.animations.topControl.opacity,
{ toValue: 0 }
),
Animated.timing(
this.animations.topControl.marginTop,
{ toValue: 20 }
),
Animated.timing(
this.animations.bottomControl.opacity,
{ toValue: 0 }
),
Animated.timing(
this.animations.bottomControl.marginBottom,
{ toValue: 20 }
),
]).start();
}
/**
* Animation to show controls...opposite of
* above...move onto the screen and then
* fade in.
*/
showControlAnimation() {
Animated.parallel([
Animated.timing(
this.animations.topControl.opacity,
{ toValue: 1 }
),
Animated.timing(
this.animations.topControl.marginTop,
{ toValue: 0 }
),
Animated.timing(
this.animations.bottomControl.opacity,
{ toValue: 1 }
),
Animated.timing(
this.animations.bottomControl.marginBottom,
{ toValue: 0 }
),
]).start();
}
/**
* Loop animation to spin loader icon. If not loading then stop loop.
*/
loadAnimation() {
if ( this.state.loading ) {
Animated.sequence([
Animated.timing(
this.animations.loader.rotate,
{
toValue: this.animations.loader.MAX_VALUE,
duration: 1500,
easing: Easing.linear,
}
),
Animated.timing(
this.animations.loader.rotate,
{
toValue: 0,
duration: 0,
easing: Easing.linear,
}
),
]).start( this.loadAnimation.bind( this ) );
}
}
/**
* Function to hide the controls. Sets our
* state then calls the animation.
*/
_hideControls() {
if(this.mounted) {
let state = this.state;
state.showControls = false;
this.hideControlAnimation();
this.setState( state );
}
}
/**
* Function to toggle controls based on
* current state.
*/
_toggleControls() {
let state = this.state;
state.showControls = ! state.showControls;
if ( state.showControls ) {
this.showControlAnimation();
this.setControlTimeout();
}
else {
this.hideControlAnimation();
this.clearControlTimeout();
}
this.setState( state );
}
/**
* Toggle fullscreen changes resizeMode on
* the <Video> component then updates the
* isFullscreen state.
*/
_toggleFullscreen() {
let state = this.state;
state.isFullscreen = !state.isFullscreen;
// if (this.props.toggleResizeModeOnFullscreen) {
// state.resizeMode = state.isFullscreen === true ? 'cover' : 'contain';
// }
if (state.isFullscreen) {
typeof this.events.onEnterFullscreen === 'function' && this.events.onEnterFullscreen();
}
else {
typeof this.events.onExitFullscreen === 'function' && this.events.onExitFullscreen();
}
this.setState( state );
}
/**
* Toggle playing state on <Video> component
*/
_togglePlayPause() {
let state = this.state;
state.paused = !state.paused;
if (state.paused) {
typeof this.events.onPause === 'function' && this.events.onPause();
}
else {
typeof this.events.onPlay === 'function' && this.events.onPlay();
}
this.setState( state );
}
/**
* Toggle between showing time remaining or
* video duration in the timer control
*/
_toggleTimer() {
let state = this.state;
state.showTimeRemaining = ! state.showTimeRemaining;
this.setState( state );
}
/**
* The default 'onBack' function pops the navigator
* and as such the video player requires a
* navigator prop by default.
*/
_onBack() {
if ( this.props.navigator && this.props.navigator.pop ) {
this.props.navigator.pop();
}
else {
console.warn( 'Warning: _onBack requires navigator property to function. Either modify the onBack prop or pass a navigator prop' );
}
}
/**
* Calculate the time to show in the timer area
* based on if they want to see time remaining
* or duration. Formatted to look as 00:00.
*/
calculateTime() {
if ( this.state.showTimeRemaining ) {
const time = this.state.duration - this.state.currentTime;
return `-${ this.formatTime( time ) }`;
}
return this.formatTime( this.state.currentTime );
}
/**
* Format a time string as mm:ss
*
* @param {int} time time in milliseconds
* @return {string} formatted time string in mm:ss format
*/
formatTime( time = 0 ) {
const symbol = this.state.showRemainingTime ? '-' : '';
time = Math.min(
Math.max( time, 0 ),
this.state.duration
);
const formattedMinutes = _.padStart( Math.floor( time / 60 ).toFixed( 0 ), 2, 0 );
const formattedSeconds = _.padStart( Math.floor( time % 60 ).toFixed( 0 ), 2 , 0 );
return `${ symbol }${ formattedMinutes }:${ formattedSeconds }`;
}
/**
* 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}
*/
setSeekerPosition( position = 0 ) {
let state = this.state;
position = this.constrainToSeekerMinMax( position );
state.seekerFillWidth = position;
state.seekerPosition = position;
if ( ! state.seeking ) {
state.seekerOffset = position
};
this.setState( state );
}
/**
* Contrain the location of the seeker to the
* min/max value based on how big the
* seeker is.
*
* @param {float} val position of seeker handle in px
* @return {float} contrained position of seeker handle in px
*/
constrainToSeekerMinMax( val = 0 ) {
if ( val <= 0 ) {
return 0;
}
else if ( val >= this.player.seekerWidth ) {
return this.player.seekerWidth;
}
return val;
}
/**
* Calculate the position that the seeker should be
* at along its track.
*
* @return {float} position of seeker handle in px based on currentTime
*/
calculateSeekerPosition() {
const percent = this.state.currentTime / this.state.duration;
return this.player.seekerWidth * percent;
}
/**
* Return the time that the video should be at
* based on where the seeker handle is.
*
* @return {float} time in ms based on seekerPosition.
*/
calculateTimeFromSeekerPosition() {
const percent = this.state.seekerPosition / this.player.seekerWidth;
return this.state.duration * percent;
}
/**
* Seek to a time in the video.
*
* @param {float} time time to seek to in ms
*/
seekTo( time = 0 ) {
let state = this.state;
state.currentTime = time;
this.player.ref.seek(time);
this.setState( state );
}
/**
* Set the position of the volume slider
*
* @param {float} position position of the volume handle in px
*/
setVolumePosition( position = 0 ) {
let state = this.state;
position = this.constrainToVolumeMinMax( position );
state.volumePosition = position + this.player.iconOffset;
state.volumeFillWidth = position;
state.volumeTrackWidth = this.player.volumeWidth - state.volumeFillWidth;
if ( state.volumeFillWidth < 0 ) {
state.volumeFillWidth = 0;
}
if ( state.volumeTrackWidth > 150 ) {
state.volumeTrackWidth = 150;
}
this.setState( state );
}
/**
* Constrain the volume bar to the min/max of
* its track's width.
*
* @param {float} val position of the volume handle in px
* @return {float} contrained position of the volume handle in px
*/
constrainToVolumeMinMax( val = 0 ) {
if ( val <= 0 ) {
return 0;
}
else if ( val >= this.player.volumeWidth + 9 ) {
return this.player.volumeWidth + 9;
}
return val;
}
/**
* Get the volume based on the position of the
* volume object.
*
* @return {float} volume level based on volume handle position
*/
calculateVolumeFromVolumePosition() {
return this.state.volumePosition / this.player.volumeWidth;
}
/**
* Get the position of the volume handle based
* on the volume
*
* @return {float} volume handle position in px based on volume
*/
calculateVolumePositionFromVolume() {
return this.player.volumeWidth * this.state.volume;
}
/**
| -------------------------------------------------------
| React Component functions
| -------------------------------------------------------
|
| Here we're initializing our listeners and getting
| the component ready using the built-in React
| Component methods
|
*/
/**
* Before mounting, init our seekbar and volume bar
* pan responders.
*/
componentDidMount() {
this.initSeekPanResponder();
this.initVolumePanResponder();
}
/**
* To allow basic playback management from the outside
* we have to handle possible props changes to state changes
*/
UNSAFE_componentWillReceiveProps(nextProps, prevState) {
if (this.state.paused !== nextProps.paused ) {
this.setState({
paused: nextProps.paused
})
}
if (this.state.title !== nextProps.title ) {
this.setState({
title: nextProps.title
})
}
if (this.state.source !== nextProps.source ) {
this.setState({
source: nextProps.source
})
}
if(this.styles.videoStyle !== nextProps.videoStyle){
this.styles.videoStyle = nextProps.videoStyle;
}
if(this.styles.containerStyle !== nextProps.style){
this.styles.containerStyle = nextProps.style;
}
}
/**
* Upon mounting, calculate the position of the volume
* bar based on the volume property supplied to it.
*/
componentDidMount() {
const position = this.calculateVolumePositionFromVolume();
let state = this.state;
this.setVolumePosition( position );
state.volumeOffset = position;
this.mounted = true;
this.setState( state );
}
/**
* When the component is about to unmount kill the
* timeout less it fire in the prev/next scene
*/
componentWillUnmount() {
this.mounted = false;
this.clearControlTimeout();
}
/**
* Get our seekbar responder going
*/
initSeekPanResponder() {
this.player.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 ) => {
let state = this.state;
this.clearControlTimeout();
state.seeking = true;
this.setState( state );
},
/**
* When panning, update the seekbar position, duh.
*/
onPanResponderMove: ( evt, gestureState ) => {
const position = this.state.seekerOffset + gestureState.dx;
this.setSeekerPosition( 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 = this.calculateTimeFromSeekerPosition();
let state = this.state;
if ( time >= state.duration && ! state.loading ) {
state.paused = true;
this.events.onEnd();
} else {
this.seekTo( time );
this.setControlTimeout();
state.seeking = false;
}
this.setState( state );
}
});
}
/**
* Initialize the volume pan responder.
*/
initVolumePanResponder() {
this.player.volumePanResponder = PanResponder.create({
onStartShouldSetPanResponder: ( evt, gestureState ) => true,
onMoveShouldSetPanResponder: ( evt, gestureState ) => true,
onPanResponderGrant: ( evt, gestureState ) => {
this.clearControlTimeout();
},
/**
* Update the volume as we change the position.
* If we go to 0 then turn on the mute prop
* to avoid that weird static-y sound.
*/
onPanResponderMove: ( evt, gestureState ) => {
let state = this.state;
const position = this.state.volumeOffset + gestureState.dx;
this.setVolumePosition( position );
state.volume = this.calculateVolumeFromVolumePosition();
if ( state.volume <= 0 ) {
state.muted = true;
}
else {
state.muted = false;
}
this.setState( state );
},
/**
* Update the offset...
*/
onPanResponderRelease: ( evt, gestureState ) => {
let state = this.state;
state.volumeOffset = state.volumePosition;
this.setControlTimeout();
this.setState( state );
}
});
}
onSeekBar = ( evt, gestureState ) => {
const time = evt.seekTime;
let state = this.state;
if ( time >= state.duration && ! state.loading ) {
state.paused = true;
this.events.onEnd();
} else {
this.seekTo( time );
this.setControlTimeout();
state.seeking = false;
}
this.setState( state );
}
/**
| -------------------------------------------------------
| Rendering
| -------------------------------------------------------
|
| This section contains all of our render methods.
| In addition to the typical React render func
| we also have all the render methods for
| the controls.
|
*/
/**
* Standard render control function that handles
* everything except the sliders. Adds a
* consistent <TouchableHighlight>
* wrapper and styling.
*/
renderControl( children, callback, style = {} ) {
return (
<TouchableHighlight
underlayColor="transparent"
activeOpacity={ 0.2 }
onPress={()=>{
this.resetControlTimeout();
callback();
}}
style={[
styles.controls.control,
style
]}
>
{ children }
</TouchableHighlight>
);
}
/**
* Renders an empty control, used to disable a control without breaking the view layout.
*/
renderNullControl() {
return (
<View style={[ styles.controls.control ]} />
);
}
/**
* Groups the top bar controls together in an animated
* view and spaces them out.
*/
renderTopControls() {
const backControl = this.props.disableBack ? this.renderNullControl() : this.renderBack();
// const volumeControl = this.props.disableVolume ? this.renderNullControl() : this.renderVolume();
const fullscreenControl = this.props.disableFullscreen ? this.renderNullControl() : this.renderFullscreen();
return(
<Animated.View style={[
styles.controls.top,
{
opacity: this.animations.topControl.opacity,
marginTop: this.animations.topControl.marginTop,
}
]}>
<ImageBackground
source={ require( './assets/img/top-vignette.png' ) }
style={[ styles.controls.column ]}
imageStyle={[ styles.controls.vignette ]}>
<SafeAreaView style={styles.controls.topControlGroup}>
{backControl}
{this.renderTitle()}
<View style={styles.controls.pullRight}>
{/* {volumeControl} */}
{fullscreenControl}
</View>
</SafeAreaView>
</ImageBackground>
</Animated.View>
);
}
/**
* Back button control
*/
renderBack() {
return this.renderControl(
<Image
source={ require( './assets/img/back.png' ) }
style={ styles.controls.back }
/>,
this.events.onBack,
styles.controls.back
);
}
renderForward() {
return this.renderControl(
<>
<Text style={{color : '#FFFFFF', fontSize: 8,fontWeight: '200', position:'absolute', top: 21, left:22}}>15</Text>
<Image
source={ require( './assets/img/forward.png' ) }
style={ styles.controls.forward }
/>
</>,
this.events.onForward,
styles.controls.forward
)
}
renderRewind() {
return this.renderControl(
<>
<Text style={{color : '#FFFFFF', fontSize: 8,fontWeight: '200', position:'absolute', top: 21, left:23}}>15</Text>
<Image
source={ require( './assets/img/backward.png' ) }
style={ styles.controls.backward }
/>
</>,
this.events.onRewind,
styles.controls.backward
)
}
/**
* Render the volume slider and attach the pan handlers
*/
renderVolume() {
return (
<View style={ styles.volume.container }>
<View style={[
styles.volume.fill,
{ width: this.state.volumeFillWidth }
]}/>
<View style={[
styles.volume.track,
{ width: this.state.volumeTrackWidth }
]}/>
<View
style={[
styles.volume.handle,
{ left: this.state.volumePosition }
]}
{ ...this.player.volumePanResponder.panHandlers }
>
<Image style={ styles.volume.icon } source={ require( './assets/img/volume.png' ) } />
</View>
</View>
);
}
/**
* Render fullscreen toggle and set icon based on the fullscreen state.
*/
renderFullscreen() {
let source = this.state.isFullscreen === true ? require( './assets/img/shrink.png' ) : require( './assets/img/expand.png' );
return this.renderControl(
<Image source={ source } />,
this.methods.toggleFullscreen,
styles.controls.fullscreen
);
}
/**
* Render bottom control group and wrap it in a holder
*/
renderBottomControls() {
const timerControl = this.props.disableTimer ? this.renderNullControl() : this.renderTimer();
const seekbarControl = this.props.disableSeekbar ? this.renderNullControl() : this.renderSeekbar();
const playPauseControl = this.props.disablePlayPause ? this.renderNullControl() : this.renderPlayPause();
const volumeControl = this.props.disableVolume ? this.renderNullControl() : this.renderVolume();
const forwardControl = this.renderForward()
const rewindControl = this.renderRewind()
return(
<Animated.View style={[
styles.controls.bottom,
{
opacity: this.animations.bottomControl.opacity,
marginBottom: this.animations.bottomControl.marginBottom,
}
]}>
<ImageBackground
source={ require( './assets/img/bottom-vignette.png' ) }
style={[ styles.controls.column ]}
imageStyle={[ styles.controls.vignette ]}>
{ seekbarControl }
<SafeAreaView
style={[styles.controls.row, styles.controls.bottomControlGroup]}>
<View style={{flexDirection:'row'}}>
{rewindControl}
{playPauseControl}
{forwardControl}</View>
{volumeControl}
{timerControl}
</SafeAreaView>
</ImageBackground>
</Animated.View>
);
}
/**
* Render the seekbar and attach its handlers
*/
renderSeekbar() {
return (
<ProgressBar
currentTime={this.state.currentTime}
duration={this.state.duration > 0 ? this.state.duration : 0}
onSlideStart={this.methods.togglePlayPause}
onSlideComplete={this.methods.togglePlayPause}
onSlideCapture={this.onSeekBar}
/>
);
}
/**
* Render the play/pause button and show the respective icon
*/
renderPlayPause() {
let source = this.state.paused === true ? require( './assets/img/play.png' ) : require( './assets/img/pause.png' );
return this.renderControl(
<Image source={ source } />,
this.methods.togglePlayPause,
styles.controls.playPause
);
}
/**
* Render our title...if supplied.
*/
renderTitle() {
if ( this.opts.title ) {
return (
<View style={[
styles.controls.control,
styles.controls.title,
]}>
<Text style={[
styles.controls.text,
styles.controls.titleText
]} numberOfLines={ 1 }>
{ this.state.title || '' }
</Text>
</View>
);
}
return null;
}
/**
* Show our timer.
*/
renderTimer() {
return this.renderControl(
<Text style={ styles.controls.timerText }>
{ this.calculateTime() }
</Text>,
this.methods.toggleTimer,
styles.controls.timer
);
}
/**
* Show loading icon
*/
renderLoader() {
if ( this.state.loading ) {
return (
<View style={ styles.loader.container }>
<Animated.Image source={ require( './assets/img/loader-icon.png' ) } style={[
styles.loader.icon,
{ transform: [
{ rotate: this.animations.loader.rotate.interpolate({
inputRange: [ 0, 360 ],
outputRange: [ '0deg', '360deg' ]
})}
]}
]} />
</View>
);
}
return null;
}
renderError() {
if ( this.state.error ) {
return (
<View />
);
}
return null;
}
/**
* Provide all of our options and render the whole component.
*/
render() {
return (
<TouchableHighlight
onPress={ this.events.onScreenTouch }
style={[ styles.player.container, this.styles.containerStyle ]}
>
<View style={[ styles.player.container, this.styles.containerStyle ]}>
<Video
ref={videoPlayer => (this.player.ref = videoPlayer)}
{ ...this.props }
resizeMode={ this.state.resizeMode }
volume={ this.state.volume }
paused={ this.state.paused }
muted={ this.state.muted }
rate={ this.state.rate }
onLoadStart={ this.events.onLoadStart }
onProgress={ this.events.onProgress }
onError={ this.events.onError }
onLoad={ this.events.onLoad }
onEnd={ this.events.onEnd }
playWhenInactive={this.props.playWhenInactive}
style={[ styles.player.video, this.styles.videoStyle ]}
source={ this.state.source || this.props.source }
/>
{ this.renderError() }
{ this.renderTopControls() }
{ this.renderLoader() }
{ this.renderBottomControls() }
</View>
</TouchableHighlight>
);
}
}
/**
* This object houses our styles. There's player
* specific styles and control specific ones.
* And then there's volume/seeker styles.
*/
const styles = {
player: StyleSheet.create({
container: {
backgroundColor: '#000',
flex: 1,
alignSelf: 'stretch',
justifyContent: 'space-between',
},
video: {
overflow: 'hidden',
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
},
}),
error: StyleSheet.create({
container: {
backgroundColor: 'rgba( 0, 0, 0, 0.5 )',
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
justifyContent: 'center',
alignItems: 'center',
},
icon: {
marginBottom: 16,
},
text: {
backgroundColor: 'transparent',
color: '#f27474'
},
}),
loader: StyleSheet.create({
container: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
alignItems: 'center',
justifyContent: 'center',
},
}),
controls: StyleSheet.create({
row: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
height: null,
width: null,
},
column: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
height: null,
width: null,
},
forward: {
height: 25,
width: 25,
bottom : 2,
right: 2
},
backward:{
height: 25,
width: 25,
bottom : 2,
},
vignette: {
resizeMode: 'stretch'
},
control: {
padding: 16,
},
text: {
backgroundColor: 'transparent',
color: '#FFF',
fontSize: 14,
textAlign: 'center',
},
pullRight: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
top: {
flex: 1,
alignItems: 'stretch',
justifyContent: 'flex-start',
},
bottom: {
alignItems: 'stretch',
flex: 2,
justifyContent: 'flex-end',
},
topControlGroup: {
alignSelf: 'stretch',
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: 'row',
width: null,
margin: 12,
marginBottom: 18,
},
bottomControlGroup: {
alignSelf: 'stretch',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 0,
},
volume: {
flexDirection: 'row',
},
fullscreen: {
flexDirection: 'row',
transform :[{ rotate: "90deg" }]
},
playPause: {
paddingLeft:20,
paddingRight:0,
},
title: {
alignItems: 'center',
flex: 0.6,
flexDirection: 'column',
padding: 0,
},
titleText: {
textAlign: 'center',
},
timer: {
width: 80,
},
timerText: {
backgroundColor: 'transparent',
color: '#FFF',
fontSize: 11,
textAlign: 'right',
},
}),
volume: StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'flex-start',
flexDirection: 'row',
height: 1,
marginLeft: 20,
marginRight: 20,
},
track: {
backgroundColor: '#333',
height: 1,
marginLeft: 7,
},
fill: {
backgroundColor: '#FFF',
height: 1,
},
handle: {
position: 'absolute',
marginTop: -24,
marginLeft: -24,
padding: 16,
},
icon: {
marginLeft:7
}
}),
seekbar: StyleSheet.create({
container: {
alignSelf: 'stretch',
height: 28,
marginLeft: 20,
marginRight: 20
},
track: {
backgroundColor: '#FFF',
height: 2,
position: 'relative',
top: 14,
width: '100%'
},
fill: {
backgroundColor: '#FFF',
height: 2,
width: '100%'
},
handle: {
position: 'absolute',
marginLeft: -7,
height: 28,
width: 28,
},
circle: {
borderRadius: 12,
position: 'relative',
top: 8, left: 8,
height: 12,
width: 12,
},
})
};