chore(sample): review seeker in sample (#3787)
This commit is contained in:
		| @@ -7,10 +7,8 @@ import { | ||||
|   TouchableOpacity, | ||||
|   View, | ||||
|   ActivityIndicator, | ||||
|   PanResponder, | ||||
|   ToastAndroid, | ||||
|   Platform, | ||||
|   PanResponderInstance, | ||||
|   Alert, | ||||
| } from 'react-native'; | ||||
|  | ||||
| @@ -53,6 +51,7 @@ import styles from './styles'; | ||||
| import AudioTrackSelector from './components/AudioTracksSelector'; | ||||
| import TextTrackSelector from './components/TextTracksSelector'; | ||||
| import VideoTrackSelector from './components/VideoTracksSelector'; | ||||
| import Seeker from './components/Seeker'; | ||||
|  | ||||
| type AdditionnalSourceInfo = { | ||||
|   textTracks: TextTracks; | ||||
| @@ -77,10 +76,6 @@ interface StateType { | ||||
|   fullscreen: true; | ||||
|   decoration: true; | ||||
|   isLoading: boolean; | ||||
|   seekerFillWidth: number; | ||||
|   seekerPosition: number; | ||||
|   seekerOffset: number; | ||||
|   seeking: boolean; | ||||
|   audioTracks: Array<AudioTrack>; | ||||
|   textTracks: Array<TextTrack>; | ||||
|   videoTracks: Array<VideoTrack>; | ||||
| @@ -93,6 +88,7 @@ interface StateType { | ||||
|   useCache: boolean; | ||||
|   poster?: string; | ||||
|   showNotificationControls: boolean; | ||||
|   isSeeking: boolean; | ||||
| } | ||||
|  | ||||
| class VideoPlayer extends Component { | ||||
| @@ -109,10 +105,6 @@ class VideoPlayer extends Component { | ||||
|     fullscreen: true, | ||||
|     decoration: true, | ||||
|     isLoading: false, | ||||
|     seekerFillWidth: 0, | ||||
|     seekerPosition: 0, | ||||
|     seekerOffset: 0, | ||||
|     seeking: false, | ||||
|     audioTracks: [], | ||||
|     textTracks: [], | ||||
|     videoTracks: [], | ||||
| @@ -127,10 +119,9 @@ class VideoPlayer extends Component { | ||||
|     useCache: false, | ||||
|     poster: undefined, | ||||
|     showNotificationControls: false, | ||||
|     isSeeking: false, | ||||
|   }; | ||||
|  | ||||
|   seekerWidth = 0; | ||||
|  | ||||
|   // internal usage change to index if you want to select tracks by index instead of lang | ||||
|   textTracksSelectionBy = 'index'; | ||||
|  | ||||
| @@ -271,7 +262,6 @@ class VideoPlayer extends Component { | ||||
|   ); | ||||
|  | ||||
|   video?: VideoRef; | ||||
|   seekPanResponder?: PanResponderInstance; | ||||
|  | ||||
|   popupInfo = () => { | ||||
|     VideoDecoderProperties.getWidevineLevel().then((widevineLevel: number) => { | ||||
| @@ -300,24 +290,13 @@ class VideoPlayer extends Component { | ||||
|     this.onVideoTracks(data); | ||||
|   }; | ||||
|  | ||||
|   updateSeeker = () => { | ||||
|     // put this code in timeout as because it may be put just after a setState | ||||
|     setTimeout(() => { | ||||
|       const position = this.calculateSeekerPosition(); | ||||
|       this.setSeekerPosition(position); | ||||
|     }, 1); | ||||
|   }; | ||||
|  | ||||
|   onProgress = (data: OnProgressData) => { | ||||
|     this.setState({currentTime: data.currentTime}); | ||||
|     if (!this.state.seeking) { | ||||
|       this.updateSeeker(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   onSeek = (data: OnSeekData) => { | ||||
|     this.setState({isSeeking: false}); | ||||
|     this.setState({currentTime: data.currentTime}); | ||||
|     this.updateSeeker(); | ||||
|   }; | ||||
|  | ||||
|   onVideoLoadStart = () => { | ||||
| @@ -407,13 +386,6 @@ class VideoPlayer extends Component { | ||||
|     this.setState({paused: !event.hasAudioFocus}); | ||||
|   }; | ||||
|  | ||||
|   getCurrentTimePercentage = () => { | ||||
|     if (this.state.currentTime > 0 && this.state.duration !== 0) { | ||||
|       return this.state.currentTime / this.state.duration; | ||||
|     } | ||||
|     return 0; | ||||
|   }; | ||||
|  | ||||
|   toast = (visible: boolean, message: string) => { | ||||
|     if (visible) { | ||||
|       if (Platform.OS === 'android') { | ||||
| @@ -501,166 +473,20 @@ class VideoPlayer extends Component { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   componentDidMount() { | ||||
|     this.initSeekPanResponder(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 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); | ||||
|       }, | ||||
|     }); | ||||
|   videoSeek(position: number) { | ||||
|     this.setState({isSeeking: true}); | ||||
|     this.video?.seek(position); | ||||
|   } | ||||
|  | ||||
|   renderSeekBar() { | ||||
|     if (!this.seekPanResponder) { | ||||
|       return null; | ||||
|     } | ||||
|     const seekerStyle = [ | ||||
|       styles.seekbarFill, | ||||
|       { | ||||
|         width: this.state.seekerFillWidth > 0 ? this.state.seekerFillWidth : 0, | ||||
|         backgroundColor: '#FFF', | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
|     const seekerPositionStyle = [ | ||||
|       styles.seekbarHandle, | ||||
|       { | ||||
|         left: this.state.seekerPosition > 0 ? this.state.seekerPosition : 0, | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
|     const seekerPointerStyle = [ | ||||
|       styles.seekbarCircle, | ||||
|       {backgroundColor: '#FFF'}, | ||||
|     ]; | ||||
|  | ||||
|     return ( | ||||
|       <View | ||||
|         style={styles.seekbarContainer} | ||||
|         {...this.seekPanResponder.panHandlers} | ||||
|         {...styles.generalControls}> | ||||
|         <View | ||||
|           style={styles.seekbarTrack} | ||||
|           onLayout={event => | ||||
|             (this.seekerWidth = event.nativeEvent.layout.width) | ||||
|           } | ||||
|           pointerEvents={'none'}> | ||||
|           <View style={seekerStyle} pointerEvents={'none'} /> | ||||
|         </View> | ||||
|         <View style={seekerPositionStyle} pointerEvents={'none'}> | ||||
|           <View style={seekerPointerStyle} pointerEvents={'none'} /> | ||||
|         </View> | ||||
|       </View> | ||||
|       <Seeker | ||||
|         currentTime={this.state.currentTime} | ||||
|         duration={this.state.duration} | ||||
|         isLoading={this.state.isLoading} | ||||
|         videoSeek={prop => this.videoSeek(prop)} | ||||
|         isUISeeking={this.state.isSeeking} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
							
								
								
									
										155
									
								
								examples/basic/src/components/Seeker.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								examples/basic/src/components/Seeker.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| 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; | ||||
| } | ||||
|  | ||||
| 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) { | ||||
|       console.log('update position from currentTime'); | ||||
|       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> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default Seeker; | ||||
		Reference in New Issue
	
	Block a user