Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using seek bar or slider for seeking #217

Open
Usman-02501 opened this issue Sep 27, 2024 · 12 comments
Open

Using seek bar or slider for seeking #217

Usman-02501 opened this issue Sep 27, 2024 · 12 comments

Comments

@Usman-02501
Copy link

Is seek bar is available into the VLC player or do I have to add the it manually
A code sample or some tutorial reference will be appreciated

@SarangGit
Copy link

SarangGit commented Sep 29, 2024

This is a component i created for the controls on VLCPlayer.js (Tested on Android only)

Supported Actions: [SeekBar, Loading, Error, Timer, Play/Pause, Skip 10Sec+/-, Select Audio Track, Select Subtitle Track, FullScreen, Orientation, AutoHide Controls, Show Controls on Tap]

import React, {useState, useRef, useEffect} from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Dimensions,
  StatusBar,
  Image,
  TouchableWithoutFeedback,
} from 'react-native';
import Slider from '@react-native-community/slider';
import {VLCPlayer} from 'react-native-vlc-media-player';
import {ActivityIndicator, Divider, IconButton, Menu} from 'react-native-paper';
import {primary} from '../theme/theme';
import {useFocusEffect, useNavigation} from '@react-navigation/native';
import Orientation from 'react-native-orientation-locker';

function msToTime(duration) {
  var seconds = Math.floor((duration / 1000) % 60),
    minutes = Math.floor((duration / (1000 * 60)) % 60),
    hours = Math.floor((duration / (1000 * 60 * 60)) % 24);

  hours = hours < 10 ? '0' + hours : hours;
  minutes = minutes < 10 ? '0' + minutes : minutes;
  seconds = seconds < 10 ? '0' + seconds : seconds;

  return hours + ':' + minutes + ':' + seconds;
}
let {width, height} = Dimensions.get('window');
const VideoPlayer = ({file_url, isNetwork, onFullScreen}) => {
  const navigation = useNavigation();
  const resizeModeList = ['none', 'fill', 'contain', 'cover', 'scale-down'];
  const [url, setURL] = useState(file_url);
  const playerRef = useRef(null);
  const [isFullScreen, setFullScreen] = useState(false);
  const [isBuffering, setIsBuffering] = useState(true);
  const [isError, setIsError] = useState(false);
  const [paused, setPaused] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [audioTracks, setAudioTracks] = useState([]);
  const [subTracks, setSubTracks] = useState([]);
  const [audioMenuOpen, setAudioMenuOpen] = useState(false);
  const [subtitleMenuOpen, setSubtitleMenuOpen] = useState(false);
  const [sliderValue, setSliderValue] = useState(0);
  const [isSeeking, setIsSeeking] = useState(false);
  const [selectedAudioTrack, setSelectedAudioTrack] = useState(-1);
  const [selectedSubtitle, setSelectedSubtitle] = useState(-1);
  const [resizeMode, setResizeMode] = useState(0);
  const [orientation, setOrientation] = useState('landscape');

  const [controlsVisible, setControlsVisible] = useState(true);
  const [hideControlsTimeout, setHideControlsTimeout] = useState(null);

  useFocusEffect(
    React.useCallback(() => {
      // On component focus (when entering the screen)
      return () => {
        // When component is unfocused (navigating away), lock to portrait
        Orientation.lockToPortrait();
        StatusBar.setHidden(false); // Make sure the status bar is shown
      };
    }, [navigation]),
  );

  // Handle fullscreen toggle
  const handleFullscreenToggle = () => {
    onFullScreen(!isFullScreen);
    if (isFullScreen) {
      Orientation.lockToPortrait(); // Unlock orientation when exiting fullscreen
      setFullScreen(false);
      StatusBar.setHidden(false);
      setOrientation('portrait');
    } else {
      Orientation.lockToPortrait(); // Lock orientation to landscape when entering fullscreen
      setFullScreen(true);
      StatusBar.setHidden(true);
      setOrientation('portrait');
    }
  };

  // Toggle between portrait and landscape
  const handleOrientationToggle = () => {
    if (orientation === 'landscape') {
      Orientation.lockToPortrait(); // Switch to portrait
      setOrientation('portrait');
    } else {
      StatusBar.setHidden(true);
      Orientation.lockToLandscape(); // Switch to landscape
      setOrientation('landscape');
    }
  };

  // Handle playback state changes (play, pause)
  const cycleResizeMode = () => {
    setResizeMode(prevMode => (prevMode + 1) % resizeModeList.length);
  };

  // Update the current playback time
  const onProgress = data => {
    if (!isSeeking) {
      //   console.log('progess', data);
      setCurrentTime(data.currentTime);
      setSliderValue(data.position);
    }
  };

  // Handle video load and get duration
  const onVideoLoad = data => {
    console.log(data);
    setAudioTracks(data.audioTracks || []);
    setSubTracks(data.textTracks || []);

    setIsBuffering(false);
    setDuration(data.duration);
  };

  // Handle seeking
  const onSlidingStart = () => {
    setIsSeeking(true);
  };

  const seekToTime = timeInMSeconds => {
    if (playerRef.current) {
      playerRef.current.seek((currentTime + timeInMSeconds) / 1000);
    } else {
      console.log('Seek time is out of bounds');
    }
  };

  const toggleAudioMenu = () => {
    setAudioMenuOpen(!audioMenuOpen);
  };
  const toggleSubtitleMenu = () => {
    setSubtitleMenuOpen(!subtitleMenuOpen);
  };

  const updateSelectedTrack = id => {
    setSelectedAudioTrack(id);
    toggleAudioMenu();
  };

  const updateSelectedSubtitle = id => {
    setSelectedSubtitle(id);
    toggleSubtitleMenu();
  };

  const onSlidingComplete = value => {
    const seekTime = value * duration;
    playerRef.current.seek(seekTime / 1000);
    setCurrentTime(seekTime);
    setIsSeeking(false);
  };

  const onValueChange = value => {
    console.log('on valueCHnage', value);
    setCurrentTime(value * duration);
  };

  const backButtonClick = () => {
    if (isFullScreen) {
      handleFullscreenToggle();
    } else {
      navigation.goBack();
    }
  };

  const showControlsTemporarily = () => {
    setControlsVisible(true);

    if (hideControlsTimeout) {
      clearTimeout(hideControlsTimeout);
    }

    const timeout = setTimeout(() => {
      setControlsVisible(false);
    }, 3000); // Hide controls after 3 seconds

    setHideControlsTimeout(timeout);
  };

  // Function to toggle control visibility on user tap
  const toggleControls = () => {
    if (!(isBuffering || isError)) {
      if (controlsVisible) {
        setControlsVisible(false);
        if (hideControlsTimeout) clearTimeout(hideControlsTimeout);
      } else {
        showControlsTemporarily();
      }
    }
  };
  console.log('URL', url);
  return (
    <TouchableWithoutFeedback onPress={toggleControls}>
      <View
        style={{
          flex: 1,
          backgroundColor: '#000',
          width: isFullScreen ? Dimensions.get('window').width : '100%',
          height: isFullScreen ? Dimensions.get('window').height : 260, // Dynamically apply height based on screen mode
        }}>
        <VLCPlayer
          ref={playerRef}
          source={{
            uri: url,
            isNetwork,
          }}
          style={{
            width: isFullScreen ? Dimensions.get('window').width : '100%',
            height: isFullScreen ? Dimensions.get('window').height : 260,
          }}
          paused={paused}
          autoplay={true}
          resizeMode={'none'}
          onProgress={onProgress}
          audioTrack={selectedAudioTrack}
          textTrack={selectedSubtitle}
          onLoad={onVideoLoad}
          onError={() => setIsError(true)}
        />
        {controlsVisible && (
          <View style={styles.controls}>
            <View
              style={{
                flex: 1,
                flexDirection: 'column',
                justifyContent: 'space-between',
                alignItems: 'center',
                width: '100%',
              }}>
              <View
                style={{
                  flexDirection: 'row',
                  justifyContent: 'space-between',
                  width: '100%',
                }}>
                {/* {!isFullScreen && ( */}
                <IconButton
                  icon="arrow-left"
                  iconColor={'white'}
                  size={20}
                  onPress={backButtonClick}
                />
                {/* )} */}
              </View>
              <View>
                {isBuffering ? (
                  <View
                    style={{
                      flexDirection: 'column',
                      justifyContent: 'center',
                      alignItems: 'center',
                    }}>
                    <ActivityIndicator animating={true} color={'white'} />
                    <Text style={{color: 'white'}}>Loading</Text>
                  </View>
                ) : null}
                {!isBuffering && !isError ? (
                  <View
                    style={{
                      flexDirection: 'row',
                      justifyContent: 'space-around',
                      alignItems: 'center',
                    }}>
                    <IconButton
                      icon="rewind-10"
                      mode={'outlined'}
                      iconColor="white"
                      size={20}
                      onPress={() => seekToTime(-10000)}
                    />
                    {paused ? (
                      <IconButton
                        icon="play"
                        mode={'outlined'}
                        iconColor="white"
                        size={25}
                        onPress={() => setPaused(false)}
                      />
                    ) : (
                      <IconButton
                        icon="pause"
                        mode={'outlined'}
                        iconColor="white"
                        size={25}
                        onPress={() => setPaused(true)}
                      />
                    )}

                    <IconButton
                      icon="fast-forward-10"
                      mode={'outlined'}
                      iconColor="white"
                      size={20}
                      onPress={() => seekToTime(10000)}
                    />
                  </View>
                ) : null}

                {isError ? (
                  <View
                    style={{
                      flexDirection: 'column',
                      justifyContent: 'center',
                      alignItems: 'center',
                    }}>
                    <Text style={{color: 'white'}}>Error...</Text>
                  </View>
                ) : null}
              </View>
              <View
                style={{
                  flexDirection: 'column',
                  width: '100%',
                }}>
                <View
                  style={{
                    flexDirection: 'row',
                    justifyContent: 'flex-end',
                    alignItems: 'center',
                  }}>
                  {/* //timer */}
                  <Text style={styles.timeText}>
                    {msToTime(currentTime)}/{msToTime(duration)}
                  </Text>
                </View>

                <Slider
                  style={styles.slider}
                  minimumValue={0}
                  maximumValue={1}
                  value={sliderValue}
                  onValueChange={onValueChange}
                  onSlidingStart={onSlidingStart}
                  onSlidingComplete={onSlidingComplete}
                  minimumTrackTintColor={primary.main}
                  maximumTrackTintColor={primary.light}
                  thumbTintColor={primary.main}
                />
                <View
                  style={{
                    flexDirection: 'row',
                    justifyContent: 'space-between',
                    alignItems: 'center',
                  }}>
                  <View
                    style={{
                      flexDirection: 'row',
                      justifyContent: 'space-around',
                      alignItems: 'center',
                    }}>
                    <Menu
                      visible={audioMenuOpen}
                      onDismiss={() => setAudioMenuOpen(false)}
                      anchor={
                        <IconButton
                          icon="playlist-music"
                          iconColor={'white'}
                          size={20}
                          onPress={toggleAudioMenu}
                        />
                      }>
                      {audioTracks.length ? (
                        audioTracks.map((track, i) => {
                          return (
                            <Menu.Item
                              key={track.id}
                              titleStyle={{
                                color:
                                  track.id == selectedAudioTrack
                                    ? primary.main
                                    : 'black',
                              }}
                              onPress={() => updateSelectedTrack(track.id)}
                              title={track.name}
                            />
                          );
                        })
                      ) : (
                        <Menu.Item
                          style={styles.menuText}
                          onPress={() => toggleAudioMenu()}
                          title={'No Audio Tracks'}
                        />
                      )}
                    </Menu>
                    <Menu
                      style={{flex: 1}}
                      visible={subtitleMenuOpen}
                      onDismiss={() => setSubtitleMenuOpen(false)}
                      anchor={
                        <IconButton
                          icon="subtitles-outline"
                          iconColor={'white'}
                          size={20}
                          onPress={toggleSubtitleMenu}
                        />
                      }>
                      {subTracks.length ? (
                        subTracks.map((track, i) => {
                          return (
                            <Menu.Item
                              key={track.id}
                              titleStyle={{
                                color:
                                  track.id == selectedSubtitle
                                    ? primary.main
                                    : 'black',
                              }}
                              onPress={() => updateSelectedSubtitle(track.id)}
                              title={track.name}
                            />
                          );
                        })
                      ) : (
                        <Menu.Item
                          onPress={toggleSubtitleMenu}
                          title={'No Subtitles'}
                        />
                      )}
                    </Menu>
                  </View>

                  <View style={{flexDirection: 'row'}}>
                    {/* <IconButton
                  icon={'cellphone-screenshot'}
                  iconColor={'white'}
                  size={20}
                  onPress={cycleResizeMode}
                /> */}
                    {isFullScreen && (
                      <IconButton
                        icon={'screen-rotation'}
                        iconColor={'white'}
                        size={20}
                        onPress={handleOrientationToggle}
                      />
                    )}
                    <IconButton
                      icon={isFullScreen ? 'fullscreen-exit' : 'fullscreen'}
                      iconColor={'white'}
                      size={20}
                      onPress={handleFullscreenToggle}
                    />
                  </View>
                </View>
              </View>
            </View>
          </View>
        )}
      </View>
    </TouchableWithoutFeedback>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000',
    width: '100%',
    height: 260,
  },
  fullScreenContainer: {
    backgroundColor: '#000',
    width: width,
    height: height,
  },
  videoPlayer: {
    width: '100%',
    height: 260,
  },
  fullScreenVideoPlayer: {
    width: width,
    height: height,
  },
  controls: {
    position: 'absolute',
    width: '100%',
    height: '100%',
    bottom: 0,
  },
  playPauseButton: {
    marginBottom: 10,
    backgroundColor: '#1EB1FC',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 5,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: 'bold',
  },

  timeContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    width: '90%',
    marginTop: 10,
  },
  timeText: {
    color: '#fff',
    paddingHorizontal: 10,
  },
});

export default VideoPlayer;

@SarangGit
Copy link

Using the above component under a ScrollView like this

<ScrollView
        ref={scrollRef}
        style={{flex: 1}}
        showsVerticalScrollIndicator={false}
        scrollEnabled={isFullScreen ? false : true}
        contentContainerStyle={{
          backgroundColor: 'black',
          justifyContent: 'center',
          alignItems: 'center',
        }}>
        <VideoPlayer
          file_url={file.url}
          isNetwork={true}
          onFullScreen={setIsFullScreen}
        />
</ScrollView>

PS: Sorry i don't know about the standards we should share the code on git. Just wrote this yesterday, thought should share it with community. Suggestions are welcome.

@Usman-02501
Copy link
Author

Thanks its working but generate this error. Cannot read property 'lockToPortrait' of null.

@SarangGit
Copy link

Means Orientation is null. Please verify you are importing 'react-native-orientation-locker'

@Usman-02501
Copy link
Author

Usman-02501 commented Oct 1, 2024

It is not working in expo use npx expo install expo-screen-orientation this expo library or i am run my project in development build its work in my expo project.

@SarangGit
Copy link

SarangGit commented Oct 1, 2024

Hi, issue is with AndroidManifest.xml file. need to add android:screenOrientation="portrait" in Tag.

Also add Orientation && before every Orientation.lockToPortrait()/ Orientation.lockToLandscape(); just to be on safer side.

LikeOrientation && Orientation.lockToPortrait() or Orientation && Orientation.lockToLandscape()

@shanmukhaaditya10
Copy link

This is a component i created for the controls on VLCPlayer.js (Tested on Android only)

Supported Actions: [SeekBar, Loading, Error, Timer, Play/Pause, Skip 10Sec+/-, Select Audio Track, Select Subtitle Track, FullScreen, Orientation, AutoHide Controls, Show Controls on Tap]

import React, {useState, useRef, useEffect} from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Dimensions,
  StatusBar,
  Image,
  TouchableWithoutFeedback,
} from 'react-native';
import Slider from '@react-native-community/slider';
import {VLCPlayer} from 'react-native-vlc-media-player';
import {ActivityIndicator, Divider, IconButton, Menu} from 'react-native-paper';
import {primary} from '../theme/theme';
import {useFocusEffect, useNavigation} from '@react-navigation/native';
import Orientation from 'react-native-orientation-locker';

function msToTime(duration) {
  var seconds = Math.floor((duration / 1000) % 60),
    minutes = Math.floor((duration / (1000 * 60)) % 60),
    hours = Math.floor((duration / (1000 * 60 * 60)) % 24);

  hours = hours < 10 ? '0' + hours : hours;
  minutes = minutes < 10 ? '0' + minutes : minutes;
  seconds = seconds < 10 ? '0' + seconds : seconds;

  return hours + ':' + minutes + ':' + seconds;
}
let {width, height} = Dimensions.get('window');
const VideoPlayer = ({file_url, isNetwork, onFullScreen}) => {
  const navigation = useNavigation();
  const resizeModeList = ['none', 'fill', 'contain', 'cover', 'scale-down'];
  const [url, setURL] = useState(file_url);
  const playerRef = useRef(null);
  const [isFullScreen, setFullScreen] = useState(false);
  const [isBuffering, setIsBuffering] = useState(true);
  const [isError, setIsError] = useState(false);
  const [paused, setPaused] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [audioTracks, setAudioTracks] = useState([]);
  const [subTracks, setSubTracks] = useState([]);
  const [audioMenuOpen, setAudioMenuOpen] = useState(false);
  const [subtitleMenuOpen, setSubtitleMenuOpen] = useState(false);
  const [sliderValue, setSliderValue] = useState(0);
  const [isSeeking, setIsSeeking] = useState(false);
  const [selectedAudioTrack, setSelectedAudioTrack] = useState(-1);
  const [selectedSubtitle, setSelectedSubtitle] = useState(-1);
  const [resizeMode, setResizeMode] = useState(0);
  const [orientation, setOrientation] = useState('landscape');

  const [controlsVisible, setControlsVisible] = useState(true);
  const [hideControlsTimeout, setHideControlsTimeout] = useState(null);

  useFocusEffect(
    React.useCallback(() => {
      // On component focus (when entering the screen)
      return () => {
        // When component is unfocused (navigating away), lock to portrait
        Orientation.lockToPortrait();
        StatusBar.setHidden(false); // Make sure the status bar is shown
      };
    }, [navigation]),
  );

  // Handle fullscreen toggle
  const handleFullscreenToggle = () => {
    onFullScreen(!isFullScreen);
    if (isFullScreen) {
      Orientation.lockToPortrait(); // Unlock orientation when exiting fullscreen
      setFullScreen(false);
      StatusBar.setHidden(false);
      setOrientation('portrait');
    } else {
      Orientation.lockToPortrait(); // Lock orientation to landscape when entering fullscreen
      setFullScreen(true);
      StatusBar.setHidden(true);
      setOrientation('portrait');
    }
  };

  // Toggle between portrait and landscape
  const handleOrientationToggle = () => {
    if (orientation === 'landscape') {
      Orientation.lockToPortrait(); // Switch to portrait
      setOrientation('portrait');
    } else {
      StatusBar.setHidden(true);
      Orientation.lockToLandscape(); // Switch to landscape
      setOrientation('landscape');
    }
  };

  // Handle playback state changes (play, pause)
  const cycleResizeMode = () => {
    setResizeMode(prevMode => (prevMode + 1) % resizeModeList.length);
  };

  // Update the current playback time
  const onProgress = data => {
    if (!isSeeking) {
      //   console.log('progess', data);
      setCurrentTime(data.currentTime);
      setSliderValue(data.position);
    }
  };

  // Handle video load and get duration
  const onVideoLoad = data => {
    console.log(data);
    setAudioTracks(data.audioTracks || []);
    setSubTracks(data.textTracks || []);

    setIsBuffering(false);
    setDuration(data.duration);
  };

  // Handle seeking
  const onSlidingStart = () => {
    setIsSeeking(true);
  };

  const seekToTime = timeInMSeconds => {
    if (playerRef.current) {
      playerRef.current.seek((currentTime + timeInMSeconds) / 1000);
    } else {
      console.log('Seek time is out of bounds');
    }
  };

  const toggleAudioMenu = () => {
    setAudioMenuOpen(!audioMenuOpen);
  };
  const toggleSubtitleMenu = () => {
    setSubtitleMenuOpen(!subtitleMenuOpen);
  };

  const updateSelectedTrack = id => {
    setSelectedAudioTrack(id);
    toggleAudioMenu();
  };

  const updateSelectedSubtitle = id => {
    setSelectedSubtitle(id);
    toggleSubtitleMenu();
  };

  const onSlidingComplete = value => {
    const seekTime = value * duration;
    playerRef.current.seek(seekTime / 1000);
    setCurrentTime(seekTime);
    setIsSeeking(false);
  };

  const onValueChange = value => {
    console.log('on valueCHnage', value);
    setCurrentTime(value * duration);
  };

  const backButtonClick = () => {
    if (isFullScreen) {
      handleFullscreenToggle();
    } else {
      navigation.goBack();
    }
  };

  const showControlsTemporarily = () => {
    setControlsVisible(true);

    if (hideControlsTimeout) {
      clearTimeout(hideControlsTimeout);
    }

    const timeout = setTimeout(() => {
      setControlsVisible(false);
    }, 3000); // Hide controls after 3 seconds

    setHideControlsTimeout(timeout);
  };

  // Function to toggle control visibility on user tap
  const toggleControls = () => {
    if (!(isBuffering || isError)) {
      if (controlsVisible) {
        setControlsVisible(false);
        if (hideControlsTimeout) clearTimeout(hideControlsTimeout);
      } else {
        showControlsTemporarily();
      }
    }
  };
  console.log('URL', url);
  return (
    <TouchableWithoutFeedback onPress={toggleControls}>
      <View
        style={{
          flex: 1,
          backgroundColor: '#000',
          width: isFullScreen ? Dimensions.get('window').width : '100%',
          height: isFullScreen ? Dimensions.get('window').height : 260, // Dynamically apply height based on screen mode
        }}>
        <VLCPlayer
          ref={playerRef}
          source={{
            uri: url,
            isNetwork,
          }}
          style={{
            width: isFullScreen ? Dimensions.get('window').width : '100%',
            height: isFullScreen ? Dimensions.get('window').height : 260,
          }}
          paused={paused}
          autoplay={true}
          resizeMode={'none'}
          onProgress={onProgress}
          audioTrack={selectedAudioTrack}
          textTrack={selectedSubtitle}
          onLoad={onVideoLoad}
          onError={() => setIsError(true)}
        />
        {controlsVisible && (
          <View style={styles.controls}>
            <View
              style={{
                flex: 1,
                flexDirection: 'column',
                justifyContent: 'space-between',
                alignItems: 'center',
                width: '100%',
              }}>
              <View
                style={{
                  flexDirection: 'row',
                  justifyContent: 'space-between',
                  width: '100%',
                }}>
                {/* {!isFullScreen && ( */}
                <IconButton
                  icon="arrow-left"
                  iconColor={'white'}
                  size={20}
                  onPress={backButtonClick}
                />
                {/* )} */}
              </View>
              <View>
                {isBuffering ? (
                  <View
                    style={{
                      flexDirection: 'column',
                      justifyContent: 'center',
                      alignItems: 'center',
                    }}>
                    <ActivityIndicator animating={true} color={'white'} />
                    <Text style={{color: 'white'}}>Loading</Text>
                  </View>
                ) : null}
                {!isBuffering && !isError ? (
                  <View
                    style={{
                      flexDirection: 'row',
                      justifyContent: 'space-around',
                      alignItems: 'center',
                    }}>
                    <IconButton
                      icon="rewind-10"
                      mode={'outlined'}
                      iconColor="white"
                      size={20}
                      onPress={() => seekToTime(-10000)}
                    />
                    {paused ? (
                      <IconButton
                        icon="play"
                        mode={'outlined'}
                        iconColor="white"
                        size={25}
                        onPress={() => setPaused(false)}
                      />
                    ) : (
                      <IconButton
                        icon="pause"
                        mode={'outlined'}
                        iconColor="white"
                        size={25}
                        onPress={() => setPaused(true)}
                      />
                    )}

                    <IconButton
                      icon="fast-forward-10"
                      mode={'outlined'}
                      iconColor="white"
                      size={20}
                      onPress={() => seekToTime(10000)}
                    />
                  </View>
                ) : null}

                {isError ? (
                  <View
                    style={{
                      flexDirection: 'column',
                      justifyContent: 'center',
                      alignItems: 'center',
                    }}>
                    <Text style={{color: 'white'}}>Error...</Text>
                  </View>
                ) : null}
              </View>
              <View
                style={{
                  flexDirection: 'column',
                  width: '100%',
                }}>
                <View
                  style={{
                    flexDirection: 'row',
                    justifyContent: 'flex-end',
                    alignItems: 'center',
                  }}>
                  {/* //timer */}
                  <Text style={styles.timeText}>
                    {msToTime(currentTime)}/{msToTime(duration)}
                  </Text>
                </View>

                <Slider
                  style={styles.slider}
                  minimumValue={0}
                  maximumValue={1}
                  value={sliderValue}
                  onValueChange={onValueChange}
                  onSlidingStart={onSlidingStart}
                  onSlidingComplete={onSlidingComplete}
                  minimumTrackTintColor={primary.main}
                  maximumTrackTintColor={primary.light}
                  thumbTintColor={primary.main}
                />
                <View
                  style={{
                    flexDirection: 'row',
                    justifyContent: 'space-between',
                    alignItems: 'center',
                  }}>
                  <View
                    style={{
                      flexDirection: 'row',
                      justifyContent: 'space-around',
                      alignItems: 'center',
                    }}>
                    <Menu
                      visible={audioMenuOpen}
                      onDismiss={() => setAudioMenuOpen(false)}
                      anchor={
                        <IconButton
                          icon="playlist-music"
                          iconColor={'white'}
                          size={20}
                          onPress={toggleAudioMenu}
                        />
                      }>
                      {audioTracks.length ? (
                        audioTracks.map((track, i) => {
                          return (
                            <Menu.Item
                              key={track.id}
                              titleStyle={{
                                color:
                                  track.id == selectedAudioTrack
                                    ? primary.main
                                    : 'black',
                              }}
                              onPress={() => updateSelectedTrack(track.id)}
                              title={track.name}
                            />
                          );
                        })
                      ) : (
                        <Menu.Item
                          style={styles.menuText}
                          onPress={() => toggleAudioMenu()}
                          title={'No Audio Tracks'}
                        />
                      )}
                    </Menu>
                    <Menu
                      style={{flex: 1}}
                      visible={subtitleMenuOpen}
                      onDismiss={() => setSubtitleMenuOpen(false)}
                      anchor={
                        <IconButton
                          icon="subtitles-outline"
                          iconColor={'white'}
                          size={20}
                          onPress={toggleSubtitleMenu}
                        />
                      }>
                      {subTracks.length ? (
                        subTracks.map((track, i) => {
                          return (
                            <Menu.Item
                              key={track.id}
                              titleStyle={{
                                color:
                                  track.id == selectedSubtitle
                                    ? primary.main
                                    : 'black',
                              }}
                              onPress={() => updateSelectedSubtitle(track.id)}
                              title={track.name}
                            />
                          );
                        })
                      ) : (
                        <Menu.Item
                          onPress={toggleSubtitleMenu}
                          title={'No Subtitles'}
                        />
                      )}
                    </Menu>
                  </View>

                  <View style={{flexDirection: 'row'}}>
                    {/* <IconButton
                  icon={'cellphone-screenshot'}
                  iconColor={'white'}
                  size={20}
                  onPress={cycleResizeMode}
                /> */}
                    {isFullScreen && (
                      <IconButton
                        icon={'screen-rotation'}
                        iconColor={'white'}
                        size={20}
                        onPress={handleOrientationToggle}
                      />
                    )}
                    <IconButton
                      icon={isFullScreen ? 'fullscreen-exit' : 'fullscreen'}
                      iconColor={'white'}
                      size={20}
                      onPress={handleFullscreenToggle}
                    />
                  </View>
                </View>
              </View>
            </View>
          </View>
        )}
      </View>
    </TouchableWithoutFeedback>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000',
    width: '100%',
    height: 260,
  },
  fullScreenContainer: {
    backgroundColor: '#000',
    width: width,
    height: height,
  },
  videoPlayer: {
    width: '100%',
    height: 260,
  },
  fullScreenVideoPlayer: {
    width: width,
    height: height,
  },
  controls: {
    position: 'absolute',
    width: '100%',
    height: '100%',
    bottom: 0,
  },
  playPauseButton: {
    marginBottom: 10,
    backgroundColor: '#1EB1FC',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 5,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: 'bold',
  },

  timeContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    width: '90%',
    marginTop: 10,
  },
  timeText: {
    color: '#fff',
    paddingHorizontal: 10,
  },
});

export default VideoPlayer;

The seek function is not working for me, sometimes onProgress handler returns the same current time even if video is playing

@ciestowp
Copy link

This is a component i created for the controls on VLCPlayer.js (Tested on Android only)
Supported Actions: [SeekBar, Loading, Error, Timer, Play/Pause, Skip 10Sec+/-, Select Audio Track, Select Subtitle Track, FullScreen, Orientation, AutoHide Controls, Show Controls on Tap]

import React, {useState, useRef, useEffect} from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Dimensions,
  StatusBar,
  Image,
  TouchableWithoutFeedback,
} from 'react-native';
import Slider from '@react-native-community/slider';
import {VLCPlayer} from 'react-native-vlc-media-player';
import {ActivityIndicator, Divider, IconButton, Menu} from 'react-native-paper';
import {primary} from '../theme/theme';
import {useFocusEffect, useNavigation} from '@react-navigation/native';
import Orientation from 'react-native-orientation-locker';

function msToTime(duration) {
  var seconds = Math.floor((duration / 1000) % 60),
    minutes = Math.floor((duration / (1000 * 60)) % 60),
    hours = Math.floor((duration / (1000 * 60 * 60)) % 24);

  hours = hours < 10 ? '0' + hours : hours;
  minutes = minutes < 10 ? '0' + minutes : minutes;
  seconds = seconds < 10 ? '0' + seconds : seconds;

  return hours + ':' + minutes + ':' + seconds;
}
let {width, height} = Dimensions.get('window');
const VideoPlayer = ({file_url, isNetwork, onFullScreen}) => {
  const navigation = useNavigation();
  const resizeModeList = ['none', 'fill', 'contain', 'cover', 'scale-down'];
  const [url, setURL] = useState(file_url);
  const playerRef = useRef(null);
  const [isFullScreen, setFullScreen] = useState(false);
  const [isBuffering, setIsBuffering] = useState(true);
  const [isError, setIsError] = useState(false);
  const [paused, setPaused] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [audioTracks, setAudioTracks] = useState([]);
  const [subTracks, setSubTracks] = useState([]);
  const [audioMenuOpen, setAudioMenuOpen] = useState(false);
  const [subtitleMenuOpen, setSubtitleMenuOpen] = useState(false);
  const [sliderValue, setSliderValue] = useState(0);
  const [isSeeking, setIsSeeking] = useState(false);
  const [selectedAudioTrack, setSelectedAudioTrack] = useState(-1);
  const [selectedSubtitle, setSelectedSubtitle] = useState(-1);
  const [resizeMode, setResizeMode] = useState(0);
  const [orientation, setOrientation] = useState('landscape');

  const [controlsVisible, setControlsVisible] = useState(true);
  const [hideControlsTimeout, setHideControlsTimeout] = useState(null);

  useFocusEffect(
    React.useCallback(() => {
      // On component focus (when entering the screen)
      return () => {
        // When component is unfocused (navigating away), lock to portrait
        Orientation.lockToPortrait();
        StatusBar.setHidden(false); // Make sure the status bar is shown
      };
    }, [navigation]),
  );

  // Handle fullscreen toggle
  const handleFullscreenToggle = () => {
    onFullScreen(!isFullScreen);
    if (isFullScreen) {
      Orientation.lockToPortrait(); // Unlock orientation when exiting fullscreen
      setFullScreen(false);
      StatusBar.setHidden(false);
      setOrientation('portrait');
    } else {
      Orientation.lockToPortrait(); // Lock orientation to landscape when entering fullscreen
      setFullScreen(true);
      StatusBar.setHidden(true);
      setOrientation('portrait');
    }
  };

  // Toggle between portrait and landscape
  const handleOrientationToggle = () => {
    if (orientation === 'landscape') {
      Orientation.lockToPortrait(); // Switch to portrait
      setOrientation('portrait');
    } else {
      StatusBar.setHidden(true);
      Orientation.lockToLandscape(); // Switch to landscape
      setOrientation('landscape');
    }
  };

  // Handle playback state changes (play, pause)
  const cycleResizeMode = () => {
    setResizeMode(prevMode => (prevMode + 1) % resizeModeList.length);
  };

  // Update the current playback time
  const onProgress = data => {
    if (!isSeeking) {
      //   console.log('progess', data);
      setCurrentTime(data.currentTime);
      setSliderValue(data.position);
    }
  };

  // Handle video load and get duration
  const onVideoLoad = data => {
    console.log(data);
    setAudioTracks(data.audioTracks || []);
    setSubTracks(data.textTracks || []);

    setIsBuffering(false);
    setDuration(data.duration);
  };

  // Handle seeking
  const onSlidingStart = () => {
    setIsSeeking(true);
  };

  const seekToTime = timeInMSeconds => {
    if (playerRef.current) {
      playerRef.current.seek((currentTime + timeInMSeconds) / 1000);
    } else {
      console.log('Seek time is out of bounds');
    }
  };

  const toggleAudioMenu = () => {
    setAudioMenuOpen(!audioMenuOpen);
  };
  const toggleSubtitleMenu = () => {
    setSubtitleMenuOpen(!subtitleMenuOpen);
  };

  const updateSelectedTrack = id => {
    setSelectedAudioTrack(id);
    toggleAudioMenu();
  };

  const updateSelectedSubtitle = id => {
    setSelectedSubtitle(id);
    toggleSubtitleMenu();
  };

  const onSlidingComplete = value => {
    const seekTime = value * duration;
    playerRef.current.seek(seekTime / 1000);
    setCurrentTime(seekTime);
    setIsSeeking(false);
  };

  const onValueChange = value => {
    console.log('on valueCHnage', value);
    setCurrentTime(value * duration);
  };

  const backButtonClick = () => {
    if (isFullScreen) {
      handleFullscreenToggle();
    } else {
      navigation.goBack();
    }
  };

  const showControlsTemporarily = () => {
    setControlsVisible(true);

    if (hideControlsTimeout) {
      clearTimeout(hideControlsTimeout);
    }

    const timeout = setTimeout(() => {
      setControlsVisible(false);
    }, 3000); // Hide controls after 3 seconds

    setHideControlsTimeout(timeout);
  };

  // Function to toggle control visibility on user tap
  const toggleControls = () => {
    if (!(isBuffering || isError)) {
      if (controlsVisible) {
        setControlsVisible(false);
        if (hideControlsTimeout) clearTimeout(hideControlsTimeout);
      } else {
        showControlsTemporarily();
      }
    }
  };
  console.log('URL', url);
  return (
    <TouchableWithoutFeedback onPress={toggleControls}>
      <View
        style={{
          flex: 1,
          backgroundColor: '#000',
          width: isFullScreen ? Dimensions.get('window').width : '100%',
          height: isFullScreen ? Dimensions.get('window').height : 260, // Dynamically apply height based on screen mode
        }}>
        <VLCPlayer
          ref={playerRef}
          source={{
            uri: url,
            isNetwork,
          }}
          style={{
            width: isFullScreen ? Dimensions.get('window').width : '100%',
            height: isFullScreen ? Dimensions.get('window').height : 260,
          }}
          paused={paused}
          autoplay={true}
          resizeMode={'none'}
          onProgress={onProgress}
          audioTrack={selectedAudioTrack}
          textTrack={selectedSubtitle}
          onLoad={onVideoLoad}
          onError={() => setIsError(true)}
        />
        {controlsVisible && (
          <View style={styles.controls}>
            <View
              style={{
                flex: 1,
                flexDirection: 'column',
                justifyContent: 'space-between',
                alignItems: 'center',
                width: '100%',
              }}>
              <View
                style={{
                  flexDirection: 'row',
                  justifyContent: 'space-between',
                  width: '100%',
                }}>
                {/* {!isFullScreen && ( */}
                <IconButton
                  icon="arrow-left"
                  iconColor={'white'}
                  size={20}
                  onPress={backButtonClick}
                />
                {/* )} */}
              </View>
              <View>
                {isBuffering ? (
                  <View
                    style={{
                      flexDirection: 'column',
                      justifyContent: 'center',
                      alignItems: 'center',
                    }}>
                    <ActivityIndicator animating={true} color={'white'} />
                    <Text style={{color: 'white'}}>Loading</Text>
                  </View>
                ) : null}
                {!isBuffering && !isError ? (
                  <View
                    style={{
                      flexDirection: 'row',
                      justifyContent: 'space-around',
                      alignItems: 'center',
                    }}>
                    <IconButton
                      icon="rewind-10"
                      mode={'outlined'}
                      iconColor="white"
                      size={20}
                      onPress={() => seekToTime(-10000)}
                    />
                    {paused ? (
                      <IconButton
                        icon="play"
                        mode={'outlined'}
                        iconColor="white"
                        size={25}
                        onPress={() => setPaused(false)}
                      />
                    ) : (
                      <IconButton
                        icon="pause"
                        mode={'outlined'}
                        iconColor="white"
                        size={25}
                        onPress={() => setPaused(true)}
                      />
                    )}

                    <IconButton
                      icon="fast-forward-10"
                      mode={'outlined'}
                      iconColor="white"
                      size={20}
                      onPress={() => seekToTime(10000)}
                    />
                  </View>
                ) : null}

                {isError ? (
                  <View
                    style={{
                      flexDirection: 'column',
                      justifyContent: 'center',
                      alignItems: 'center',
                    }}>
                    <Text style={{color: 'white'}}>Error...</Text>
                  </View>
                ) : null}
              </View>
              <View
                style={{
                  flexDirection: 'column',
                  width: '100%',
                }}>
                <View
                  style={{
                    flexDirection: 'row',
                    justifyContent: 'flex-end',
                    alignItems: 'center',
                  }}>
                  {/* //timer */}
                  <Text style={styles.timeText}>
                    {msToTime(currentTime)}/{msToTime(duration)}
                  </Text>
                </View>

                <Slider
                  style={styles.slider}
                  minimumValue={0}
                  maximumValue={1}
                  value={sliderValue}
                  onValueChange={onValueChange}
                  onSlidingStart={onSlidingStart}
                  onSlidingComplete={onSlidingComplete}
                  minimumTrackTintColor={primary.main}
                  maximumTrackTintColor={primary.light}
                  thumbTintColor={primary.main}
                />
                <View
                  style={{
                    flexDirection: 'row',
                    justifyContent: 'space-between',
                    alignItems: 'center',
                  }}>
                  <View
                    style={{
                      flexDirection: 'row',
                      justifyContent: 'space-around',
                      alignItems: 'center',
                    }}>
                    <Menu
                      visible={audioMenuOpen}
                      onDismiss={() => setAudioMenuOpen(false)}
                      anchor={
                        <IconButton
                          icon="playlist-music"
                          iconColor={'white'}
                          size={20}
                          onPress={toggleAudioMenu}
                        />
                      }>
                      {audioTracks.length ? (
                        audioTracks.map((track, i) => {
                          return (
                            <Menu.Item
                              key={track.id}
                              titleStyle={{
                                color:
                                  track.id == selectedAudioTrack
                                    ? primary.main
                                    : 'black',
                              }}
                              onPress={() => updateSelectedTrack(track.id)}
                              title={track.name}
                            />
                          );
                        })
                      ) : (
                        <Menu.Item
                          style={styles.menuText}
                          onPress={() => toggleAudioMenu()}
                          title={'No Audio Tracks'}
                        />
                      )}
                    </Menu>
                    <Menu
                      style={{flex: 1}}
                      visible={subtitleMenuOpen}
                      onDismiss={() => setSubtitleMenuOpen(false)}
                      anchor={
                        <IconButton
                          icon="subtitles-outline"
                          iconColor={'white'}
                          size={20}
                          onPress={toggleSubtitleMenu}
                        />
                      }>
                      {subTracks.length ? (
                        subTracks.map((track, i) => {
                          return (
                            <Menu.Item
                              key={track.id}
                              titleStyle={{
                                color:
                                  track.id == selectedSubtitle
                                    ? primary.main
                                    : 'black',
                              }}
                              onPress={() => updateSelectedSubtitle(track.id)}
                              title={track.name}
                            />
                          );
                        })
                      ) : (
                        <Menu.Item
                          onPress={toggleSubtitleMenu}
                          title={'No Subtitles'}
                        />
                      )}
                    </Menu>
                  </View>

                  <View style={{flexDirection: 'row'}}>
                    {/* <IconButton
                  icon={'cellphone-screenshot'}
                  iconColor={'white'}
                  size={20}
                  onPress={cycleResizeMode}
                /> */}
                    {isFullScreen && (
                      <IconButton
                        icon={'screen-rotation'}
                        iconColor={'white'}
                        size={20}
                        onPress={handleOrientationToggle}
                      />
                    )}
                    <IconButton
                      icon={isFullScreen ? 'fullscreen-exit' : 'fullscreen'}
                      iconColor={'white'}
                      size={20}
                      onPress={handleFullscreenToggle}
                    />
                  </View>
                </View>
              </View>
            </View>
          </View>
        )}
      </View>
    </TouchableWithoutFeedback>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000',
    width: '100%',
    height: 260,
  },
  fullScreenContainer: {
    backgroundColor: '#000',
    width: width,
    height: height,
  },
  videoPlayer: {
    width: '100%',
    height: 260,
  },
  fullScreenVideoPlayer: {
    width: width,
    height: height,
  },
  controls: {
    position: 'absolute',
    width: '100%',
    height: '100%',
    bottom: 0,
  },
  playPauseButton: {
    marginBottom: 10,
    backgroundColor: '#1EB1FC',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 5,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: 'bold',
  },

  timeContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    width: '90%',
    marginTop: 10,
  },
  timeText: {
    color: '#fff',
    paddingHorizontal: 10,
  },
});

export default VideoPlayer;

The seek function is not working for me, sometimes onProgress handler returns the same current time even if video is playing

Same issue i am facing. I tried every possible ways but didn't get solution.

@smolleyes
Copy link

hello !

i fixed this damn seeking problem for me by doing this:

1/ don t use "seek" props in VLCPlayer use "position" (wrong doc ?), position take a range not milliseconds as seek

and just pass your slider position value

exemple :

<VLCPlayer
        ref={playerRef}
        style={{ flex: 1 }}
        source={{ uri: videoUrl }}
        paused={paused}
        muted={muted}
        autoplay
        position={sliderPosition} // value between 0 and 1
        videoAspectRatio="16:9"
        onProgress={handleProgress}
        onLoad={handleLoad}
        onPlaying={handlePlaying}
      />

2/ edit ReactVlcPlayerViewManager.java and change setPosition function to this:

@ReactProp(name = PROP_POSITION)
    public void setPosition(final ReactVlcPlayerView videoView, final float position) {
        Log.d("VLCPlayer", "setPosition called with: " + position);
        // if position is zero, we don't want to seek to zero
        if (position == 0) {
            return;
        }
        videoView.setPosition(position);
    }

when i was seeking it was always returning to 0, added this check to avoid it and seek fixed !

Thanks

@cornejobarraza
Copy link
Collaborator

hello !

i fixed this damn seeking problem for me by doing this:

1/ don t use "seek" props in VLCPlayer use "position" (wrong doc ?), position take a range not milliseconds as seek

and just pass your slider position value

exemple :

<VLCPlayer
        ref={playerRef}
        style={{ flex: 1 }}
        source={{ uri: videoUrl }}
        paused={paused}
        muted={muted}
        autoplay
        position={sliderPosition} // value between 0 and 1
        videoAspectRatio="16:9"
        onProgress={handleProgress}
        onLoad={handleLoad}
        onPlaying={handlePlaying}
      />

2/ edit ReactVlcPlayerViewManager.java and change setPosition function to this:

@ReactProp(name = PROP_POSITION)
    public void setPosition(final ReactVlcPlayerView videoView, final float position) {
        Log.d("VLCPlayer", "setPosition called with: " + position);
        // if position is zero, we don't want to seek to zero
        if (position == 0) {
            return;
        }
        videoView.setPosition(position);
    }

when i was seeking it was always returning to 0, added this check to avoid it and seek fixed !

Thanks

Nice catch, would you mind creating a PR?

@smolleyes
Copy link

Hello

I'll do bro, do i also add something for the doc not talking about position ?

And normal to have seekTo commented in viewmanager also ? Doesn't build if we uncomment ... (Can fix)

Thanks

@cornejobarraza
Copy link
Collaborator

Hello

I'll do bro, do i also add something for the doc not talking about position ?

And normal to have seekTo commented in viewmanager also ? Doesn't build if we uncomment ... (Can fix)

Thanks

Yes, you can update the README section for that prop and feel free to take a look at that other issue as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants