UNPKG

react-native-webview-video

Version:

react-native-webview-video is a React Native library that provides a Video component that renders media content such as videos with custom controls for react-native and react-native-web. Support Youtube and Vimeo.

521 lines (499 loc) 22.9 kB
import { Component, createRef } from 'react'; import { Animated, Platform, View, ActivityIndicator, Pressable, Text, AppState } from 'react-native'; import { DEFAULT_USER_AGENT, DEFAULT_YOUTUBE_URL, DEFAULT_VIMEO_URL, DEFAULT_VIDEOJS_URL, getTime } from './src/helpers'; import { youtubeHTML } from './src/sources/Youtube'; import { vimeoHTML } from './src/sources/Vimeo'; import { videoJSHTML } from './src/sources/VideoJS'; import PlaySVG from './src/icons/PlaySVG'; import PauseSVG from './src/icons/PauseSVG'; import VolumeOffSVG from './src/icons/VolumeOffSVG'; import VolumeOnSVG from './src/icons/VolumeOnSVG'; import WebView from 'react-native-webview'; import ReplaySVG from './src/icons/ReplaySVG'; export default class Video extends Component { constructor(props) { super(props); this.state = { onPlay: false, playerReady: false, playerPlaying: false, progressBar: "0%", progressTime: "00:00", totalTime: "00:00", duration: 0, volumeOn: true }; this.webVideoRef = createRef(); this.imageAnimated = new Animated.Value(0); } componentDidMount() { this.AppVideoState = AppState.addEventListener('change', this._handleAppStateChange); } componentWillUnmount() { clearInterval(this.timer); this.AppVideoState.remove(); } componentDidUpdate(prevProps, prevState) { } _handleAppStateChange = currentAppState => { if (currentAppState === 'background' && this.state.playerPlaying) { this.setPlaying(); } } onImageLoad = () => { Animated.timing(this.imageAnimated, { toValue: 1, useNativeDriver: Platform.OS === "web" ? false : true }).start(({ finished }) => { }); } onMessageRecieved = event => { // console.log("Webview onMessage event ", event); try { let message; if (typeof event.nativeEvent.data == "object") { message = event.nativeEvent.data; } else { message = JSON.parse(event.nativeEvent.data); } const info = typeof message.eventType != "undefined" ? message.eventType : ""; switch (info) { case 'playerStateChange': if (message.data == 0) { this.setState({ progressTime: this.state.totalTime }); if (typeof this.props.videoEnd != "undefined") { this.props.videoEnd(); } this.setStop(); } break; case 'infoDelivery': this.getProgressTime(message); break; case "initialDelivery": this.getProgressTime(message, true); break; case 'playerReady': if (typeof this.props.videoReady != "undefined") { this.props.videoReady(); } this.setPlayerReady(); break; case 'fullScreenChange': // console.log("fullScreenChange ", message); if (typeof this.props.onFullScreenChange != "undefined") { this.props.onFullScreenChange(message.data); } break; case 'playerQualityChange': // console.log("playerQualityChange ", message); if (typeof this.props.videoQualityChange != "undefined") { this.props.videoQualityChange(message.data); } break; case 'playerError': // console.log("playerError ", message); if (typeof this.props.playerError != "undefined") { this.props.playerError(); } break; case 'playbackRateChange': // console.log("playbackRateChange ", message); if (typeof this.props.videoRateChange != "undefined") { this.props.videoRateChange(message.data); } break; default: // console.log("default ", message); break; } } catch (error) { console.warn('[react-native-video-webview]', error); } } getContent() { let loadContent = null; const videoId = typeof this.props.videoId != "undefined" ? this.props.videoId : ""; const videoType = typeof this.props.videoType != "undefined" ? this.props.videoType : ""; const videoSource = typeof this.props.videoSource != "undefined" ? this.props.videoSource : ""; switch (videoSource) { case "youtube": if (typeof this.props.useRemote != "undefined" && this.props.useRemote == true) { loadContent = { uri: DEFAULT_YOUTUBE_URL + '?videoId=' + videoId + '&videoType=' + videoType }; } else { loadContent = { html: youtubeHTML(videoId, videoType) }; } break; case "vimeo": if (typeof this.props.useRemote != "undefined" && this.props.useRemote == true) { loadContent = { uri: DEFAULT_VIMEO_URL + '?videoId=' + videoId + '&videoType=' + videoType }; } else { loadContent = { html: vimeoHTML(videoId, videoType) }; } break; case "direct": if (typeof this.props.useRemote != "undefined" && this.props.useRemote == true) { loadContent = { uri: DEFAULT_VIDEOJS_URL + '?videoId=' + videoId + '&videoType=' + videoType }; } else { loadContent = { html: videoJSHTML(videoId) }; } break; default: break; } return loadContent; } render() { const content = this.getContent(); const buttonColor = typeof this.props.buttonColor != "undefined" ? this.props.buttonColor : "#FFFFFF"; const largeButtonWidth = typeof this.props.largeButtonWidth != "undefined" ? this.props.largeButtonWidth : 60; return (<View pointerEvents={this.state.playerReady ? "auto" : "none"} style={{ alignItems: 'flex-start', alignSelf: 'flex-start', textAlign: 'flex-start', width: '100%', }}> <View pointerEvents="none" style={{ alignItems: 'flex-start', alignSelf: 'flex-start', textAlign: 'flex-start', width: '100%', aspectRatio: 16 / 9 }}> {this.renderWebview(content)} {this.renderLoading()} </View> { this.props.poster != "undefined" && !this.state.playerPlaying ? <Pressable style={{ width: '100%', aspectRatio: 16 / 9, alignItems: 'center', justifyContent: 'center', position: "absolute", left: 0, right: 0, bottom: 0, top: 0, }} onPress={() => { this.setPlaying(); }}> <Animated.Image source={{ uri: this.props.poster }} style={[{ width: '100%', aspectRatio: 16 / 9 }, { opacity: this.imageAnimated }]} onLoad={this.onImageLoad()} /> <View style={{ backgroundColor: content ? "#00000030" : "transparent", width: '100%', aspectRatio: 16 / 9, alignItems: 'center', justifyContent: 'center', position: "absolute", left: 0, right: 0, bottom: 0, top: 0, }}> { content ? <View style={{ width: largeButtonWidth, height: largeButtonWidth, flexDirection: 'column', justifyContent: 'center', alignItems: 'flex-start', }}> {!this.state.playerPlaying && this.state.onPlay ? <ReplaySVG width={largeButtonWidth} color={buttonColor} /> : <PlaySVG width={largeButtonWidth} color={buttonColor} />} </View> : null } </View> </Pressable> : null } {content != null ? this.renderControls() : null} </View>); } renderWebview(content) { return (content != null ? <WebView {...this.props} ref={this.webVideoRef} allowsFullscreenVideo={false} allowsInlineMediaPlayback scalesPageToFit={false} onMessage={this.onMessageRecieved} mediaPlaybackRequiresUserAction={false} originWhitelist={['*']} overScrollMode={"never"} nestedScrollEnabled={true} automaticallyAdjustContentInsets={true} javaScriptEnabled={true} mixedContentMode="compatibility" source={content} userAgent={ typeof this.props.forceAndroidAutoplay != "undefined" ? Platform.select({ android: DEFAULT_USER_AGENT, ios: '' }) : '' } onShouldStartLoadWithRequest={event => { return true; }} /> : null) } renderControls() { const buttonWidth = typeof this.props.buttonWidth != "undefined" ? this.props.buttonWidth : 30; const progressBarHeight = typeof this.props.progressBarHeight != "undefined" ? this.props.progressBarHeight : 8; const progressBarPadding = typeof this.props.progressBarPadding != "undefined" ? this.props.progressBarPadding : 2; const progressBarContainerHeight = typeof this.props.progressBarContainerHeight != "undefined" ? this.props.progressBarContainerHeight : 12; const progressBarContainerColor = typeof this.props.progressBarContainerColor != "undefined" ? this.props.progressBarContainerColor : "#171a1d"; const buttonColor = typeof this.props.buttonColor != "undefined" ? this.props.buttonColor : "#FFFFFF"; const textColor = typeof this.props.textColor != "undefined" ? this.props.textColor : "#a8afb5"; const textSize = typeof this.props.textSize != "undefined" ? this.props.textSize : 12; const textContainerMinWidth = typeof this.props.textContainerMinWidth != "undefined" ? this.props.textContainerMinWidth : 38; const progressBarColor = typeof this.props.progressBarColor != "undefined" ? this.props.progressBarColor : "#5fcf80" const spacing = typeof this.props.spacing != "undefined" ? this.props.spacing : 5 return <View style={[{ backgroundColor: "#55616c", padding: 8, paddingTop: 6, paddingBottom: 6 }, typeof this.props.controlsStyle != "undefined" ? this.props.controlsStyle : {}, { flex: 1, width: "100%", marginTop: -1, }]}> <View style={{ justifyContent: 'space-between', flexDirection: "row", flex: 1, }}> <Pressable style={{ width: buttonWidth, height: buttonWidth, flexDirection: 'column', justifyContent: 'center', alignItems: 'flex-start', }} onPress={() => { this.setPlaying(); }}> { this.state.playerPlaying ? <PauseSVG width={buttonWidth} color={buttonColor} /> : (!this.state.playerPlaying && this.state.onPlay ? <ReplaySVG width={buttonWidth} color={buttonColor} /> : <PlaySVG width={buttonWidth} color={buttonColor} />) } </Pressable> <Pressable style={{ flexDirection: 'column', justifyContent: 'center', alignItems: 'flex-start', width: buttonWidth, height: buttonWidth, marginLeft: spacing, marginRight: spacing }} onPress={() => { this.setVolume(); }}> { this.state.volumeOn ? <VolumeOnSVG width={buttonWidth} color={buttonColor} /> : <VolumeOffSVG width={buttonWidth} color={buttonColor} /> } </Pressable> <View style={{ flexDirection: 'column', justifyContent: 'center', alignItems: 'flex-start', flexGrow: 1, flexBasis: 0 }}> <View style={{ justifyContent: 'space-between', flexDirection: "row", flex: 1, width: '100%' }}> <View style={{ flexDirection: 'column', justifyContent: 'center', alignItems: 'flex-start', marginLeft: spacing, marginRight: spacing, minWidth: textContainerMinWidth }}> <Text style={{ fontSize: textSize, fontWeight: '600', color: textColor }}>{this.state.progressTime}</Text> </View> <View style={{ flexDirection: 'column', justifyContent: 'center', alignItems: 'flex-start', paddingLeft: spacing, paddingRight: spacing, flexGrow: 1, flexBasis: 0, }}> <View style={{ flexDirection: 'column', justifyContent: 'center', alignItems: 'flex-start', backgroundColor: progressBarContainerColor, borderRadius: progressBarContainerHeight / 2, width: "100%", height: progressBarContainerHeight }}> <View style={{ flexDirection: 'column', justifyContent: 'center', alignItems: 'flex-start', borderRadius: progressBarContainerHeight / 2, width: "100%", paddingLeft: progressBarPadding, paddingRight: progressBarPadding }}> <View style={{ flexDirection: 'column', justifyContent: 'center', alignItems: 'flex-start', backgroundColor: progressBarColor, borderRadius: progressBarHeight / 2, width: this.state.progressBar, height: progressBarHeight }} /> </View> </View> </View> <View style={{ flexDirection: 'column', justifyContent: 'center', alignItems: 'flex-start', marginLeft: spacing, marginRight: spacing, fontWeight: '600', minWidth: textContainerMinWidth }}> <Text style={{ fontSize: textSize, color: textColor }}>{this.state.totalTime}</Text> </View> </View> </View> </View> </View> } setVideoStop() { if (this.webVideoRef && this.webVideoRef.current) { this.webVideoRef.current.postMessage(JSON.stringify({ event: "stopVideo", data: null }), '*'); if (typeof this.props.videoStop != "undefined") { this.props.videoStop(); } } this.setState({ playerPlaying: false, onPlay: false, progressBar: "0%", progressTime: "00:00" }); } setVideoPause() { if (this.webVideoRef && this.webVideoRef.current) { this.webVideoRef.current.postMessage(JSON.stringify({ event: "pauseVideo", data: null }), '*'); if (this.state.playerPlaying && typeof this.props.videoPause != "undefined") { this.props.videoPause(); } } this.setState({ playerPlaying: false, onPlay: false }); } setVideoPlay() { if (this.webVideoRef && this.webVideoRef.current) { if (!this.state.playerPlaying && this.state.onPlay) { this.setState({ progressBar: "0%", progressTime: "00:00" }); } this.webVideoRef.current.postMessage(JSON.stringify({ event: "playVideo", data: null }), '*'); if (!this.state.playerPlaying && typeof this.props.videoPlaying != "undefined") { this.props.videoPlaying(); } } this.setState({ playerPlaying: true, onPlay: true }); } setPlayerReady() { this.setState({ playerReady: true, playerPlaying: false, onPlay: false, progressBar: "0%", progressTime: "00:00" }); } setStop() { this.webVideoRef.current.postMessage(JSON.stringify({ event: "stopVideo", data: null }), '*'); this.setState({ playerPlaying: false, progressBar: "100%" }); } setPlaying() { if (!this.state.playerPlaying && this.state.onPlay) { this.setState({ progressBar: "0%", progressTime: "00:00" }); } let state = this.state.playerPlaying ? "pauseVideo" : "playVideo"; this.webVideoRef.current.postMessage(JSON.stringify({ event: state, data: null }), '*'); if (!this.state.playerPlaying && typeof this.props.videoPlaying != "undefined") { this.props.videoPlaying(); } if (this.state.playerPlaying && typeof this.props.videoPause != "undefined") { this.props.videoPause(); } this.setState({ playerPlaying: !this.state.playerPlaying, onPlay: !this.state.onPlay }); } setVolume() { let state = !this.state.volumeOn ? "volumeOn" : "volumeOff"; this.webVideoRef.current.postMessage(JSON.stringify({ event: state, data: null }), '*'); this.setState({ volumeOn: !this.state.volumeOn }); } getProgressTime(message, isInit = false) { if (typeof message.data != "undefined") { let totalTime = typeof message.data.duration != "undefined" ? message.data.duration : 0; let currentTime = typeof message.data.currentTime != "undefined" ? message.data.currentTime : 0; let progressTime = getTime(currentTime); if ((progressTime != "00:00" && progressTime != this.state.progressTime) || isInit) { if (typeof this.props.videoDuration != "undefined") { this.props.videoDuration({ currentTime: currentTime, duration: totalTime }); } if (currentTime > 0) { let progressBar = ((currentTime * 100) / this.state.duration) + "%"; this.setState({ progressBar: progressBar }); } this.setState({ progressTime: progressTime }); if (isInit) { const duration = getTime(totalTime); this.setState({ totalTime: duration, duration: totalTime }); } } } } renderLoading() { return !this.state.playerReady && ( <View style={{ flex: 1, zIndex: 1000, width: "100%", aspectRatio: 16 / 9, backgroundColor: typeof this.props.loadingBackgoundColor != "undefined" ? this.props.loadingBackgoundColor : '#FEFEFE', alignItems: 'center', justifyContent: 'center', position: "absolute" }}> <ActivityIndicator animating={true} style={{ flex: 1, alignItems: 'center', justifyContent: 'center', alignSelf: "center" }} size={Platform.OS === "web" ? "large" : "small"} color={typeof this.props.loadingColor != "undefined" ? this.props.loadingColor : "#DDDDDD"} /> </View> ) } }