Created
November 21, 2018 11:09
-
-
Save samermurad/448187cb0194b939293ae91d2e42a180 to your computer and use it in GitHub Desktop.
A react-native Media Display screen that can display videos, images, audio files and pdfs, it is taken from a project that I wrote, so obviously some items are project related.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react' | |
import { View, TouchableOpacity, StyleSheet,Image,Animated, Platform, BackHandler, Button } from 'react-native' | |
import { connect } from 'react-redux' | |
import {Actions} from 'react-native-router-flux' | |
import { colors,fonts,images, styles as appStyles } from '../assets' | |
import { Screen, NAVIGATION_BAR_HEIGHT, STATUS_BAR_HEIGHT } from '../consts' | |
import { mediaType } from '../enums' | |
import Interactable from 'react-native-interactable' | |
import { CachedImage } from 'react-native-cached-image' | |
import { Player as AudioPlayer } from 'react-native-audio-toolkit' | |
import { LoaderView, AnimatedGradient } from '../views' | |
import Video from 'react-native-video' | |
import MediaControls, { PLAYER_STATES } from 'react-native-media-controls'; | |
import PDF from 'react-native-pdf'; | |
import {mediaCouldNotBeLoaded} from '../actions/mediaDisplayer.actions' | |
import AudioSession, {AudioCategories, AudioOptions, AudioModes} from 'react-native-audio-session' | |
type MediaDisplayerProps = { | |
media: Object, | |
}; | |
class MediaDisplayer extends Component<MediaDisplayerProps> { | |
interactableViewHeight = Screen.height; | |
interactableViewWidth = Screen.width; | |
interactableViewYAnimationValue = new Animated.Value(0); | |
interactableViewSnapPoints = [ | |
{y: -(this.interactableViewHeight) }, { y: 0 }, { y: this.interactableViewHeight } , | |
]; | |
opacityAnimation = this.interactableViewYAnimationValue.interpolate({ | |
inputRange: this.interactableViewSnapPoints.map((i) => i.y), | |
outputRange: [0, 1, 0 ], | |
useNativeDriver: true | |
}); | |
interactableView; | |
audioPlayer; | |
didPrepareAudio = false; | |
videoPlayer; | |
state = { | |
videoPlayer: { | |
isLoading: false, | |
isPaused: false, | |
currentTime: 0, | |
duration: 0, | |
isFullScreen: false, | |
playerState: PLAYER_STATES.PLAYING, | |
}, | |
audioPlayer: { | |
isLoading: false, | |
isPaused: false, | |
duration: 0, | |
currentTime: 0, | |
playerState: PLAYER_STATES.PLAYING, | |
}, | |
pdfViewer: { | |
page: 1, | |
scale: 1, | |
numberOfPages: 0, | |
horizontal: false, | |
width: Screen.width, | |
isLoading: true, | |
} | |
}; | |
componentDidMount() { | |
let { media: { mediaType : type } } = this.props; | |
if ((type === mediaType.audio || type === mediaType.video) && Platform.OS === 'ios') { | |
const majorVersionIOS = parseInt(Platform.Version); | |
if (majorVersionIOS > 9) | |
AudioSession.setCategoryAndMode(AudioCategories.Playback,AudioModes.MoviePlayback, AudioOptions.DuckOthers); | |
else | |
AudioSession.setCategory(AudioCategories.Playback, AudioOptions.DuckOthers); | |
AudioSession.setActive(true) | |
} | |
if (Platform.OS === 'android') | |
BackHandler.addEventListener('hardwareBackPress', this.goBackListener); | |
} | |
componentWillUnmount() { | |
let { media: { mediaType : type } } = this.props; | |
if ((type === mediaType.audio || type === mediaType.video) && Platform.OS === 'ios') { | |
const majorVersionIOS = parseInt(Platform.Version); | |
if (majorVersionIOS > 9) | |
AudioSession.setCategoryAndMode(AudioCategories.Ambient,AudioModes.Default, AudioOptions.MixWithOthers); | |
else | |
AudioSession.setCategory(AudioCategories.Ambient, AudioOptions.MixWithOthers); | |
AudioSession.setActive(false) | |
} | |
if (Platform.OS === 'android') | |
BackHandler.removeEventListener('hardwareBackPress', this.goBackListener); | |
} | |
/** Image methods */ | |
onError = (err) => this.props.mediaCouldNotBeLoaded(); | |
/** Video methods */ | |
videoOnProgress = data => { | |
const { videoPlayer } = this.state; | |
// Video Player will continue progress even if the video already ended | |
if (!videoPlayer.isLoading && videoPlayer.playerState !== PLAYER_STATES.ENDED) { | |
this.setState({ | |
videoPlayer: { | |
...videoPlayer, | |
currentTime: data.currentTime, | |
} | |
}); | |
} | |
}; | |
videoOnSeek = seek => this.videoPlayer.seek(seek); | |
videoOnSeeking = currentTime => { | |
const { videoPlayer } = this.state; | |
this.setState({ | |
videoPlayer: { | |
...videoPlayer, | |
currentTime | |
} | |
}); | |
}; | |
videoOnReplay = () => { | |
let {videoPlayer} = this.state; | |
this.setState({ | |
videoPlayer: { | |
...videoPlayer, | |
playerState: PLAYER_STATES.PLAYING | |
} | |
}); | |
this.videoPlayer.seek(0); | |
}; | |
videoOnLoad = data => { | |
let { videoPlayer } = this.state; | |
this.setState({ | |
videoPlayer: | |
{...videoPlayer, | |
duration: data.duration, | |
isLoading: false, | |
} | |
}); | |
}; | |
videoOnPaused = playerState => { | |
let {videoPlayer} = this.state; | |
this.setState({ | |
videoPlayer: { | |
...videoPlayer, | |
playerState, | |
isPaused: !videoPlayer.isPaused | |
} | |
}) | |
}; | |
videoOnLoadStart = data => { | |
let { videoPlayer } = this.state; | |
this.setState({ | |
videoPlayer: | |
{...videoPlayer, | |
isLoading: true, | |
} | |
}); | |
}; | |
videoOnEnd = () => { | |
let { videoPlayer } = this.state; | |
this.setState({ | |
videoPlayer: { | |
...videoPlayer, | |
playerState: PLAYER_STATES.ENDED | |
} | |
}); | |
}; | |
videoOnError = (error) => this.props.mediaCouldNotBeLoaded(); | |
/** Audio methods */ | |
audioPrepare() { | |
return new Promise((resolve) => { | |
let {media} = this.props; | |
if (media.mediaType === mediaType.audio) { | |
this.audioPlayer = new AudioPlayer(media.url, { autoDestroy: false }); | |
this.audioPlayer.on('error',this.audioOnError); | |
this.audioPlayer.on('ended',this.audioOnEnd); | |
this.audioPlayer.on('progress', this.audioOnProgress); | |
this.audioOnLoadStart(this.audioPlayer); | |
this.audioPlayer.play((err) => { | |
if (err) { | |
//TODO: - handle error | |
this.audioOnError(err); | |
} else { | |
this.audioOnLoad(this.audioPlayer); | |
} | |
}) | |
} | |
}) | |
} | |
audioOnLoadStart = data => { | |
let { audioPlayer } = this.state; | |
this.setState({ | |
audioPlayer: | |
{...audioPlayer, | |
isLoading: true, | |
} | |
}); | |
}; | |
audioOnLoad = data => { | |
let { audioPlayer } = this.state; | |
this.setState({ | |
audioPlayer: | |
{...audioPlayer, | |
duration: data.duration / 1000, | |
isLoading: false, | |
} | |
}); | |
}; | |
audioOnProgress = (data) => { | |
// let { currentTime } = data; | |
const { audioPlayer } = this.state; | |
// Video Player will continue progress even if the video already ended | |
if (!audioPlayer.isLoading && audioPlayer.playerState !== PLAYER_STATES.ENDED) { | |
this.setState({ | |
audioPlayer: { | |
...audioPlayer, | |
currentTime: data.currentTime / 1000, | |
} | |
}); | |
} | |
}; | |
audioOnSeek = seek => this.audioPlayer.seek(seek * 1000); | |
audioOnSeeking = currentTime => { | |
const { audioPlayer } = this.state; | |
this.setState({ | |
audioPlayer: { | |
...audioPlayer, | |
currentTime | |
} | |
}); | |
}; | |
audioOnPaused = playerState => { | |
let { audioPlayer } = this.state; | |
if (audioPlayer.isPaused) this.audioPlayer.play(); else this.audioPlayer.pause(); | |
this.setState({ | |
audioPlayer: { | |
...audioPlayer, | |
playerState, | |
isPaused: !audioPlayer.isPaused | |
} | |
}) | |
}; | |
audioOnEnd = () => { | |
let { audioPlayer } = this.state; | |
this.setState({ | |
audioPlayer: { | |
...audioPlayer, | |
playerState: PLAYER_STATES.ENDED | |
} | |
}); | |
}; | |
audioOnReplay = () => { | |
let { audioPlayer } = this.state; | |
this.setState({ | |
audioPlayer: { | |
...audioPlayer, | |
playerState: PLAYER_STATES.PLAYING | |
} | |
}); | |
this.audioPlayer.play((err) => { | |
if (err) { | |
//TODO: - handle error | |
this.audioOnError(err); | |
} else { | |
this.audioOnLoad(this.audioPlayer); | |
} | |
}) | |
}; | |
audioOnError = err => this.props.mediaCouldNotBeLoaded(); | |
/** PDF methods */ | |
pdfOnLoadComplete = (numberOfPages, filePath) => { | |
let { pdfViewer } = this.state; | |
if (pdfViewer.isLoading) { | |
this.setState({ | |
pdfViewer: { | |
...pdfViewer, | |
isLoading: false, | |
numberOfPages: numberOfPages //do not use setState, it will cause re-render | |
} | |
}) | |
} | |
}; | |
pdfOnError = err => this.props.mediaCouldNotBeLoaded(); | |
renderCloseToolbar = (isTransparent = true) => { | |
return ( | |
<View style={[styles.pdfToolbarContainer, isTransparent && styles.pdfToolbarContainerTransparent]}> | |
<View style={styles.pdfToolbarContentView}> | |
<TouchableOpacity style={styles.pdfToolbarCloseBtn} onPress={() => { | |
requestAnimationFrame(() => { | |
if (!isTransparent) | |
this.destroy(() => Actions.pop()); | |
else | |
this.interactableView.snapTo({index: 2}) | |
}) | |
}}> | |
<View style={styles.pdfToolbarCloseBtnContentView}> | |
<Image style={styles.pdfIcon} source={images.logout_white}/> | |
</View> | |
</TouchableOpacity> | |
</View> | |
</View> | |
) | |
}; | |
renderImageViewer = () => { | |
return ( | |
<View style={styles.mediaItemContainer}> | |
{this.renderCloseToolbar(true)} | |
<View style={styles.imageContentView}> | |
<CachedImage | |
loadingIndicator={LoaderView} | |
style={styles.cachedImage} | |
source={{uri: this.props.media.url}} | |
onError={this.onError} | |
renderImage={(props) => <Image source={props.source} style={styles.image}/>} | |
/> | |
</View> | |
</View> | |
) | |
}; | |
renderVideoPlayer = () => { | |
return ( | |
<View style={styles.mediaItemContainer}> | |
<View style={styles.videoContentView}> | |
<Video | |
onLoadStart={this.videoOnLoadStart} | |
onLoad={this.videoOnLoad} | |
onProgress={this.videoOnProgress} | |
onEnd={this.videoOnEnd} | |
onError={this.videoOnError} | |
paused={this.state.videoPlayer.isPaused} | |
style={styles.video} | |
ref={(el) => this.videoPlayer = el} | |
source={{uri: this.props.media.url}}> | |
</Video> | |
</View> | |
<MediaControls | |
duration={this.state.videoPlayer.duration} | |
isLoading={this.state.videoPlayer.isLoading} | |
mainColor={colors.midBlue} | |
isFullScreen={true} | |
onPaused={this.videoOnPaused} | |
onReplay={this.videoOnReplay} | |
onSeek={this.videoOnSeek} | |
onSeeking={this.videoOnSeeking} | |
playerState={ this.state.videoPlayer.playerState } | |
progress={ this.state.videoPlayer.currentTime } | |
toolbar={this.renderCloseToolbar()} | |
/> | |
</View> | |
) | |
}; | |
renderAudioPlayer() { | |
if (!this.audioPlayer && !this.didPrepareAudio) { | |
this.didPrepareAudio = true; | |
let timer = setTimeout(() => { | |
this.audioPrepare(); | |
clearTimeout(timer); | |
timer = undefined; | |
}, 100); | |
} | |
return ( | |
<AnimatedGradient style={styles.mediaItemContainer} colors={[ colors.brightOrange,colors.orange ]}> | |
<View style={styles.audioContentView}> | |
<Image style={styles.logo} source={images.logo_transparent}/> | |
</View> | |
<MediaControls | |
duration={this.state.audioPlayer.duration} | |
isLoading={this.state.audioPlayer.isLoading} | |
mainColor={colors.midBlue} | |
isFullScreen={true} | |
onPaused={this.audioOnPaused} | |
onReplay={this.audioOnReplay} | |
onSeek={this.audioOnSeek} | |
onSeeking={this.audioOnSeeking} | |
playerState={ this.state.audioPlayer.playerState } | |
progress={ this.state.audioPlayer.currentTime } | |
toolbar={this.renderCloseToolbar()} | |
/> | |
</AnimatedGradient> | |
) | |
} | |
renderPDFViewer() { | |
let source = {uri:this.props.media.url , cache:true}; | |
return ( | |
<View style={styles.pdfContentView}> | |
{this.renderCloseToolbar(false)} | |
style={styles.pdfViewer} | |
source={source} | |
scale={this.state.pdfViewer.scale} | |
page={this.state.pdfViewer.page} | |
horizontal={this.state.pdfViewer.horizontal} | |
onLoadComplete={this.pdfOnLoadComplete} | |
onPageChanged={(page, numberOfPages) => { | |
this.state.page = page; //do not use setState, it will cause re-render | |
console.log(`current page: ${page}`); | |
}} | |
onError={this.pdfOnError} | |
/> | |
</View> | |
) | |
} | |
renderMedia() { | |
let { media } = this.props; | |
if (media.mediaType === mediaType.audio) | |
return this.renderAudioPlayer(); | |
else if (media.mediaType === mediaType.image) | |
return this.renderImageViewer(); | |
else if (media.mediaType === mediaType.video) | |
return this.renderVideoPlayer(); | |
else if (media.mediaType === mediaType.file) | |
return this.renderPDFViewer(); | |
} | |
renderLoader() { | |
let { videoPlayer, audioPlayer, pdfViewer } = this.state; | |
let { media } = this.props; | |
const shouldShowLoader = | |
media.mediaType === mediaType.video && videoPlayer.isLoading || | |
media.mediaType === mediaType.audio && audioPlayer.isLoading || | |
media.mediaType === mediaType.file && pdfViewer.isLoading; | |
if (shouldShowLoader) | |
return ( | |
<View style={styles.loaderViewContainer}> | |
<LoaderView /> | |
</View> | |
) | |
} | |
onSnap({index}) { | |
if (index == 0 || index == 2) { | |
this.destroy(() => Actions.pop()) | |
} | |
}; | |
render() { | |
let { media } = this.props, | |
verticalOnly = media.mediaType === mediaType.video || media.mediaType === mediaType.audio, | |
dragEnabled = media.mediaType !== mediaType.file; | |
return ( | |
<Animated.View style={[styles.container, | |
{opacity: this.opacityAnimation} ]}> | |
<Interactable.View | |
verticalOnly={verticalOnly} | |
dragEnabled={dragEnabled} | |
ref={(el) => this.interactableView = el } | |
style={styles.interactableContainer} | |
snapPoints={this.interactableViewSnapPoints} | |
onDrag={({nativeEvent}) => console.log(nativeEvent)} | |
onSnap={({nativeEvent}) => this.onSnap(nativeEvent)} | |
animatedValueY={this.interactableViewYAnimationValue}> | |
{this.renderMedia()} | |
{this.renderLoader()} | |
</Interactable.View> | |
</Animated.View> | |
) | |
} | |
destroy(callback) { | |
if (this.videoPlayer) { | |
let { videoPlayer } = this.state; | |
this.setState({ | |
videoPlayer: { | |
...videoPlayer, | |
isPaused: true | |
} | |
},() => callback && callback()) | |
} else if (this.audioPlayer) { | |
this.audioPlayer.pause(() => { | |
this.audioPlayer.destroy(() => { | |
this.audioPlayer = undefined; | |
requestAnimationFrame(() => callback && callback()) | |
}) | |
}) | |
} else { | |
requestAnimationFrame(() => callback && callback()) | |
} | |
} | |
goBackListener = () => { | |
this.onBack(); | |
return true; | |
}; | |
onBack() { | |
this.destroy(() => Actions.pop()) | |
} | |
} | |
const styles = StyleSheet.create({ | |
container: { | |
position: 'absolute', | |
top: 0, | |
bottom: 0, | |
left: 0, | |
right: 0, | |
justifyContent: 'center', | |
alignItems: 'stretch', | |
}, | |
interactableContainer: { | |
flex:1, | |
backgroundColor: colors.black, | |
flexDirection: 'row', | |
justifyContent: 'center', | |
alignItems: 'center', | |
}, | |
mediaItemContainer: { | |
width: Screen.width, | |
height: Screen.height, | |
flex: 1, | |
}, | |
imageContentView: { | |
flex:1, | |
justifyContent:'center', | |
alignItems: 'center' | |
}, | |
cachedImage: { | |
flex:1, | |
width: '100%', | |
}, | |
image: { | |
flex:1, | |
resizeMode: Image.resizeMode.contain, | |
width: '100%', | |
}, | |
videoContentView: { | |
flex: 1, | |
}, | |
video: { | |
flex: 1, | |
alignItems: 'center', | |
justifyContent: 'center' | |
}, | |
playbackBtnsContainer: { | |
height: 150, | |
position: 'absolute', | |
bottom: 0, | |
left: 0, | |
right: 0, | |
backgroundColor: 'yellow' | |
}, | |
loaderViewContainer: { | |
position: 'absolute', | |
bottom: 0, | |
left: 0, | |
right: 0, | |
top: 0, | |
justifyContent: 'center', | |
alignItems: 'center' | |
}, | |
audioContentView: { | |
flex: 1, | |
alignItems: 'center', | |
justifyContent: 'center' | |
}, | |
logo: { | |
width: 160, | |
height: 212, | |
marginBottom: 42, | |
}, | |
pdfContentView: { | |
width: Screen.width, | |
height: Screen.height, | |
flex: 1, | |
}, | |
pdfToolbarContainer: { | |
height: NAVIGATION_BAR_HEIGHT, | |
width: '100%', | |
alignItems: 'stretch', | |
backgroundColor: colors.tangerine, | |
flexDirection: 'row', | |
alignSelf: 'flex-start', | |
...Platform.select({ | |
android: { | |
marginTop: STATUS_BAR_HEIGHT + 8, | |
} | |
}) | |
}, | |
pdfToolbarContainerTransparent: { | |
backgroundColor: colors.transparent, | |
}, | |
pdfToolbarContentView: { | |
flex: 1, | |
flexDirection: 'row', | |
alignItems: 'stretch', | |
}, | |
pdfToolbarCloseBtn: { | |
flexDirection: 'row', | |
alignItems: 'stretch', | |
}, | |
pdfToolbarCloseBtnContentView: { | |
padding: 8, | |
...Platform.select({ | |
android: { | |
marginTop: 8, | |
} | |
}), | |
alignItems: 'center', | |
justifyContent: 'center', | |
...appStyles.navBarTopMargin | |
}, | |
pdfToolbarCloseBtnText: { | |
color: colors.midBlue, | |
...fonts.iranSansLight, | |
fontSize: 20, | |
}, | |
pdfViewer: { | |
width: '100%', | |
height: '80%', | |
}, | |
pdfIcon: { | |
width: 32, | |
height: 32, | |
} | |
}); | |
const mapStateToProps = (state) => ({ | |
state | |
}); | |
const mapActionsToProps = () => ({ | |
mediaCouldNotBeLoaded: mediaCouldNotBeLoaded, | |
}); | |
export default connect(mapStateToProps,mapActionsToProps(),undefined,{ withRef: true })(MediaDisplayer) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment