chore(sample): review seeker in sample (#3787)

This commit is contained in:
Olivier Bouillet 2024-05-28 09:33:21 +02:00 committed by GitHub
parent 5059e7a7f1
commit e23e02b359
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 169 additions and 188 deletions

View File

@ -7,10 +7,8 @@ import {
TouchableOpacity, TouchableOpacity,
View, View,
ActivityIndicator, ActivityIndicator,
PanResponder,
ToastAndroid, ToastAndroid,
Platform, Platform,
PanResponderInstance,
Alert, Alert,
} from 'react-native'; } from 'react-native';
@ -53,6 +51,7 @@ import styles from './styles';
import AudioTrackSelector from './components/AudioTracksSelector'; import AudioTrackSelector from './components/AudioTracksSelector';
import TextTrackSelector from './components/TextTracksSelector'; import TextTrackSelector from './components/TextTracksSelector';
import VideoTrackSelector from './components/VideoTracksSelector'; import VideoTrackSelector from './components/VideoTracksSelector';
import Seeker from './components/Seeker';
type AdditionnalSourceInfo = { type AdditionnalSourceInfo = {
textTracks: TextTracks; textTracks: TextTracks;
@ -77,10 +76,6 @@ interface StateType {
fullscreen: true; fullscreen: true;
decoration: true; decoration: true;
isLoading: boolean; isLoading: boolean;
seekerFillWidth: number;
seekerPosition: number;
seekerOffset: number;
seeking: boolean;
audioTracks: Array<AudioTrack>; audioTracks: Array<AudioTrack>;
textTracks: Array<TextTrack>; textTracks: Array<TextTrack>;
videoTracks: Array<VideoTrack>; videoTracks: Array<VideoTrack>;
@ -93,6 +88,7 @@ interface StateType {
useCache: boolean; useCache: boolean;
poster?: string; poster?: string;
showNotificationControls: boolean; showNotificationControls: boolean;
isSeeking: boolean;
} }
class VideoPlayer extends Component { class VideoPlayer extends Component {
@ -109,10 +105,6 @@ class VideoPlayer extends Component {
fullscreen: true, fullscreen: true,
decoration: true, decoration: true,
isLoading: false, isLoading: false,
seekerFillWidth: 0,
seekerPosition: 0,
seekerOffset: 0,
seeking: false,
audioTracks: [], audioTracks: [],
textTracks: [], textTracks: [],
videoTracks: [], videoTracks: [],
@ -127,10 +119,9 @@ class VideoPlayer extends Component {
useCache: false, useCache: false,
poster: undefined, poster: undefined,
showNotificationControls: false, showNotificationControls: false,
isSeeking: false,
}; };
seekerWidth = 0;
// internal usage change to index if you want to select tracks by index instead of lang // internal usage change to index if you want to select tracks by index instead of lang
textTracksSelectionBy = 'index'; textTracksSelectionBy = 'index';
@ -271,7 +262,6 @@ class VideoPlayer extends Component {
); );
video?: VideoRef; video?: VideoRef;
seekPanResponder?: PanResponderInstance;
popupInfo = () => { popupInfo = () => {
VideoDecoderProperties.getWidevineLevel().then((widevineLevel: number) => { VideoDecoderProperties.getWidevineLevel().then((widevineLevel: number) => {
@ -300,24 +290,13 @@ class VideoPlayer extends Component {
this.onVideoTracks(data); 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) => { onProgress = (data: OnProgressData) => {
this.setState({currentTime: data.currentTime}); this.setState({currentTime: data.currentTime});
if (!this.state.seeking) {
this.updateSeeker();
}
}; };
onSeek = (data: OnSeekData) => { onSeek = (data: OnSeekData) => {
this.setState({isSeeking: false});
this.setState({currentTime: data.currentTime}); this.setState({currentTime: data.currentTime});
this.updateSeeker();
}; };
onVideoLoadStart = () => { onVideoLoadStart = () => {
@ -407,13 +386,6 @@ class VideoPlayer extends Component {
this.setState({paused: !event.hasAudioFocus}); 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) => { toast = (visible: boolean, message: string) => {
if (visible) { if (visible) {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
@ -501,166 +473,20 @@ class VideoPlayer extends Component {
); );
} }
componentDidMount() { videoSeek(position: number) {
this.initSeekPanResponder(); this.setState({isSeeking: true});
} this.video?.seek(position);
/**
* 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() { 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 ( return (
<View <Seeker
style={styles.seekbarContainer} currentTime={this.state.currentTime}
{...this.seekPanResponder.panHandlers} duration={this.state.duration}
{...styles.generalControls}> isLoading={this.state.isLoading}
<View videoSeek={prop => this.videoSeek(prop)}
style={styles.seekbarTrack} isUISeeking={this.state.isSeeking}
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>
); );
} }

View 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;