'use strict'; import React, { Component } from 'react'; import { StyleSheet, Text, TouchableOpacity, View, ActivityIndicator, PanResponder, ToastAndroid, } from 'react-native'; import { Picker } from '@react-native-picker/picker' import Video, { TextTrackType } from 'react-native-video'; class VideoPlayer extends Component { state = { rate: 1, volume: 1, muted: false, resizeMode: 'contain', duration: 0.0, currentTime: 0.0, videoWidth: 0, videoHeight: 0, paused: false, fullscreen: true, decoration: true, isLoading: false, seekerFillWidth: 0, seekerPosition: 0, seekerOffset: 0, seeking: false, audioTracks: [], textTracks: [], selectedAudioTrack: undefined, selectedTextTrack: undefined, srcListId: 0, loop: false, }; seekerWidth = 0 srcList = [ require('./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: '(hls|live) red bull tv', uri: 'https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_928.m3u8' }, { 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: '(no url) Stopped playback', uri: undefined }, { description: '(no view) no View', noView: true, }, ] video: Video; seekPanResponder: PanResponder | undefined; onLoad = (data: any) => { this.setState({ duration: data.duration, loading: false, }); this.onAudioTracks(data.audioTracks) this.onTextTracks(data.textTracks) }; onProgress = (data: any) => { if (!this.state.seeking) { const position = this.calculateSeekerPosition() this.setSeekerPosition(position) } this.setState({ currentTime: data.currentTime }) }; onVideoLoadStart = () => { console.log('onVideoLoadStart') this.setState({ isLoading: true }) } onAudioTracks = (data: any) => { const selectedTrack = data.audioTracks?.find((x: any) => { return x.selected }) this.setState({ audioTracks: data, }) if (selectedTrack?.language) { this.setState({ selectedAudioTrack: { type: 'language', value: selectedTrack?.language, }, }) } } onTextTracks = (data: any) => { const selectedTrack = data.textTracks?.find((x: any) => { return x.selected }) this.setState({ textTracks: data, }) if (selectedTrack?.language) { this.setState({ textTracks: data, selectedTextTrack: { type: 'language', value: selectedTrack?.language, }, }) } } onAspectRatio = (data: any) => { console.log('onAspectRadio called ' + JSON.stringify(data)) this.setState({ videoWidth: data.width, videoHeight: data.height, }) } onVideoBuffer = (param: any) => { console.log('onVideoBuffer') this.setState({ isLoading: param.isBuffering }) } onReadyForDisplay = () => { console.log('onReadyForDisplay') this.setState({ isLoading: false }) } onAudioBecomingNoisy = () => { this.setState({ paused: true }) }; onAudioFocusChanged = (event: { hasAudioFocus: boolean }) => { this.setState({ paused: !event.hasAudioFocus }) }; getCurrentTimePercentage = () => { if (this.state.currentTime > 0 && this.state.duration !== 0) { return this.state.currentTime / this.state.duration; } return 0; }; renderRateControl(rate: number) { const isSelected = (this.state.rate === rate); return ( { this.setState({ rate }) }}> {rate} ); } renderResizeModeControl(resizeMode: string) { const isSelected = (this.state.resizeMode === resizeMode); return ( { this.setState({ resizeMode }) }}> {resizeMode} ) } renderVolumeControl(volume: number) { const isSelected = (this.state.volume === volume); return ( { this.setState({ volume }) }}> {volume * 100}% ) } toast = (visible: boolean, message: string) => { if (visible) { ToastAndroid.showWithGravityAndOffset( message, ToastAndroid.LONG, ToastAndroid.BOTTOM, 25, 50, ) return null } return null } onError = (err: any) => { console.log(JSON.stringify(err)) this.toast(true, 'error: ' + err?.error?.code) } onEnd = () => { this.channelUp() }; toggleFullscreen() { this.setState({ fullscreen: !this.state.fullscreen }) } toggleDecoration() { this.setState({ decoration: !this.state.decoration }) if (this.state.decoration) { this.video.dismissFullscreenPlayer() } else { this.video.presentFullscreenPlayer() } } goToChannel(channel: any) { this.setState({ srcListId: channel, duration: 0.0, currentTime: 0.0, videoWidth: 0, videoHeight: 0, isLoading: false, audioTracks: [], textTracks: [], selectedAudioTrack: undefined, selectedTextTrack: undefined, }) } channelUp() { console.log('channel up') this.goToChannel((this.state.srcListId + 1) % this.srcList.length) } channelDown() { console.log('channel down') this.goToChannel((this.state.srcListId + this.srcList.length - 1) % this.srcList.length) } componentDidMount() { this.initSeekPanResponder() } renderDecorationsControl() { return ( { this.toggleDecoration() }} > {'decoration'} ) } renderFullScreenControl() { return ( { this.toggleFullscreen() }} > {'fullscreen'} ) } renderPause() { return ( { this.setState({ paused: !this.state.paused }) }} > {this.state.paused ? 'pause' : 'playing'} ) } renderRepeatModeControl() { return ( { this.setState({ loop: !this.state.loop }) }} > {this.state.loop ? 'loop enable' : 'loop disable'} ) } renderLeftControl() { return ( { this.channelDown() }} > {'ChDown'} // onTimelineUpdated ) } renderRightControl() { return ( { this.channelUp() }} > {'ChUp'} ) } /** * Render the seekbar and attach its handlers */ /** * Constrain 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} constrained position of seeker handle in px */ constrainToSeekerMinMax(val = 0) { if (val <= 0) { return 0 } else if (val >= this.seekerWidth) { return this.seekerWidth } return val } /** * 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) { const state = this.state position = this.constrainToSeekerMinMax(position) state.seekerFillWidth = position state.seekerPosition = position if (!state.seeking) { state.seekerOffset = position } this.setState(state) } /** * 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.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.seekerWidth return this.state.duration * percent } /** * Get our seekbar responder going */ initSeekPanResponder() { this.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 state = this.state // this.clearControlTimeout() const position = evt.nativeEvent.locationX this.setSeekerPosition(position) 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() const state = this.state if (time >= state.duration && !state.isLoading) { state.paused = true this.onEnd() } else { this.video?.seek(time) state.seeking = false } this.setState(state) }, }) } renderSeekBar() { if (!this.seekPanResponder) { return null } return ( (this.seekerWidth = event.nativeEvent.layout.width)} pointerEvents={'none'} > 0 ? this.state.seekerFillWidth : 0, backgroundColor: '#FFF', }, ]} pointerEvents={'none'} /> 0 ? this.state.seekerPosition : 0 }, ]} pointerEvents={'none'} > ) } IndicatorLoadingView() { if (this.state.isLoading) return else return } renderOverlay() { return ( <> {this.IndicatorLoadingView()} {this.srcList[this.state.srcListId]?.description || 'local file'} {this.renderLeftControl()} {this.renderRightControl()} {this.renderPause()} {this.renderRepeatModeControl()} {this.renderFullScreenControl()} {this.renderDecorationsControl()} {this.renderRateControl(0.25)} {this.renderRateControl(0.5)} {this.renderRateControl(1.0)} {this.renderRateControl(1.5)} {this.renderRateControl(2.0)} {this.renderVolumeControl(0.5)} {this.renderVolumeControl(1)} {this.renderVolumeControl(1.5)} {this.renderResizeModeControl('cover')} {this.renderResizeModeControl('contain')} {this.renderResizeModeControl('stretch')} {this.renderSeekBar()} AudioTrack {this.state.audioTracks?.length <= 0 ? ( empty ) : ( { console.log('on audio value change ' + itemValue) this.setState({ selectedAudioTrack: { type: 'language', value: itemValue, }, }) }} > {this.state.audioTracks.map((track) => { return ( ) })} )} TextTrack {this.state.textTracks?.length <= 0 ? ( empty ) : ( { console.log('on value change ' + itemValue) this.setState({ selectedTextTrack: { type: 'language', value: itemValue, }, }) }} > {this.state.textTracks.map((track) => ( ))} )} ) } renderVideoView() { const viewStyle = this.state.fullscreen ? styles.fullScreen : styles.halfScreen return ( ) } render() { return ( {this.srcList[this.state.srcListId]?.noView ? null : this.renderVideoView()} {this.renderOverlay()} ) } } 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, }, IndicatorStyle: { flex: 1, justifyContent: 'center', }, 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: { color: 'white', flex: 1, flexDirection: 'row', justifyContent: 'center', }, }); export default VideoPlayer