Video Controls are not working on mobile devices

I am making a custom video player in Next JS using the react-player library, in addition to the normal controls I am adding a thumbnail preview on-hover on the desktop and with onTouchMove in case of touch devices. Controls are working fine on the desktop, but when I open it on ios or Android devices video player does not seek the video to the touch location, and upon moving my finger on the seek bar it only shows the preview of the video, and slider does not change its position, instead what I want is, wherever I put my finger on seek bar during video is playing the video should start playing from there

VideoPlayer component looks like this


const VideoPlayer = () => {

  //refs for video player and video preview
  const videoRef = useRef(null);
  const previewVideoRef = useRef(null);

  const [videoState, setVideoState] = useState({
    playing: true,
    isPlaying: true, //added to preserve the playing status of the video when seeked
    played: 0,
    playbackRate: 1.0,
    seeking: false,
    duration: 0,
    loaded: 0,
  });
  const {
    playing,
    played,
    playbackRate,
    seeking,
    duration,
    isPlaying,
    loaded,
  } = videoState;

  //showing the total duration of video
  const handleDuration = (duration) => {
    setVideoState((prevValue) => ({ ...prevValue, duration }));
  };

  //pause and play the video
  const playPauseHandler = () => {
    setVideoState((prevValue) => ({
      ...prevValue,
      playing: !prevValue.playing,
      isPlaying: !prevValue.isPlaying,
    }));
  };

  //seekbar moving with video progress
  const progressHandler = (e) => {
    if (!seeking) {
      setVideoState((prevValue) => ({ ...prevValue, ...e }));
    }
  };

  //changing seekbar position

  const seekHandler = (e, newValue) => {
    const playedValue = parseFloat(newValue);
    if (Number.isFinite(playedValue)) {
      setVideoState((prevValue) => ({ ...prevValue, played: playedValue }));
      if (seeking)
        videoRef.current.seekTo(
          previewVideoRef.current.getInternalPlayer().currentTime
        );
    }
  };

  //when seekbar is dragged with a click

  const seekMouseDownHandler = () => {
    setVideoState((prevValue) => ({
      ...prevValue,
      seeking: true,
      playing: false,
    }));
  };

  //when click is released

  const seekMouseUpHandler = (e) => {
    setVideoState((prevValue) => ({
      ...prevValue,
      seeking: false,
      playing: isPlaying,
    }));
  };

  //restart the video

  const handleRestart = () => {
    videoRef.current.seekTo(0);
    setVideoState((prevValue) => ({ ...prevValue, playing: true }));
  };

  //controlling playback rate

  const handlePlaybackRateChange = (rate) => {
    setVideoState((prevValue) => ({ ...prevValue, playbackRate: rate }));
  };
  return (
    <TransformWrapper>
      {({ zoomIn, zoomOut, ...rest }) => (
        <>
          <div className="react-player">
            <Flex
              gap={0}
              align="center"
              justify="center"
              vertical={true}
              className=""
            >
              <div className="video-player-box">
                <TransformComponent>
                  <ReactPlayer
                    ref={videoRef}
                    fallback={<Loading />}
                    url="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"
                    playing={playing}
                    width="100%"
                    muted={true}
                    playsinline={true}
                    onDuration={handleDuration}
                    onProgress={progressHandler}
                    playbackRate={playbackRate}
                  />
                </TransformComponent>
              </div>
              <ReactPlayer
                url="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"
                style={{ display: "none" }}
                ref={previewVideoRef}
                playing={false}
                loop={true}
              />

              <VideoControls
                videoRef={videoRef}
                previewVideoRef={previewVideoRef}
                playing={playing}
                onPlayPause={playPauseHandler}
                played={played}
                onSeek={seekHandler}
                onSeekMouseUp={seekMouseUpHandler}
                onSeekMouseDown={seekMouseDownHandler}
                onRestart={handleRestart}
                duration={duration}
                playbackRate={playbackRate}
                onPlaybackRateChange={handlePlaybackRateChange}
                loaded={loaded}
              />
            </Flex>
          </div>
        </>
      )}
    </TransformWrapper>
  );
};

export default VideoPlayer;

and the VideoControls look like this


function parseDate(date) {
  if (date) return dayjs(date).format("hh:mm:ss A");
  else return "";
}

const calcSliderPosition = (e) => {
  let clientX;
  // Check if it's a touch event
  if (e.type === "touchmove") {
    // Use the clientX property from the first touch point
    clientX = e.touches[0].clientX;
  } else {
    // It's a mouse event, use offsetX
    clientX = e.nativeEvent.offsetX;
  }
  return (
    (clientX < 0 ? 0 : clientX / e.target.clientWidth) *
    e.target.getAttribute("max")
  );
};

const VideoControls = ({
  playing,
  videoRef,
  previewVideoRef,
  onPlayPause,
  played,
  onSeek,
  onSeekMouseUp,
  onSeekMouseDown,
  onRestart,
  duration,
  playbackRate,
  onPlaybackRateChange,
  loaded,
}) => {
  const { details } = useAppSelector((state) => state.alarms.selectedAlarm);
  const [position, setPosition] = useState(0);
  const [previewSrc, setPreviewSrc] = useState(null);

  function handlePosition(e) {
    const THUMBNAIL_MIDDLE_POINT = 88;
    const THUMBNAIL_WIDTH = 178;
    const PRECISION_VALUE = 1;
    const TOTAL_WIDTH = e.target.offsetWidth;

    let clientX;
    // Check if it's a touch event
    if (e.type === "touchmove") {
      // Use the clientX property from the first touch point
      clientX = e.touches[0].clientX - e.target.getBoundingClientRect().left;
    } else {
      // It's a mouse event, use offsetX
      clientX = e.nativeEvent.offsetX;
    }

    //change the position only when the pointer is between the video length
    if (clientX > 1 && clientX < TOTAL_WIDTH) {
      //check for extreme left thumbnail preview
      if (clientX > THUMBNAIL_MIDDLE_POINT)
        if (clientX < TOTAL_WIDTH - THUMBNAIL_MIDDLE_POINT)
          setPosition(clientX - THUMBNAIL_MIDDLE_POINT);
        else {
          let nextPosition = clientX - clientX + TOTAL_WIDTH - THUMBNAIL_WIDTH;
          if (
            nextPosition < position - PRECISION_VALUE ||
            nextPosition > position + PRECISION_VALUE
          )
            setPosition(nextPosition);
        }
      //in case there is not enough space hold the thumnail to the left
      else {
        // let nextPosition = e.nativeEvent.layerX - e.nativeEvent.offsetX;
        let nextPosition = clientX - clientX;
        if (
          nextPosition < position - PRECISION_VALUE ||
          nextPosition > position + PRECISION_VALUE
        )
          setPosition(nextPosition);
      }
    }
  }

  const generatePreview = async (e) => {
    try {
      let seekTime = calcSliderPosition(e);
      let newTime = seekTime * previewVideoRef.current.getDuration();
      let prevTime = previewVideoRef.current.getCurrentTime();

      const PRECISION_VALUE = 0.01;
      if (
        newTime > prevTime - PRECISION_VALUE &&
        newTime < prevTime + PRECISION_VALUE
      ) {
        // console.log("do not create");
      } else {
        const canvas = document.createElement("canvas");
        const context = canvas.getContext("2d");

        previewVideoRef.current.seekTo(seekTime);
        const video = previewVideoRef.current.getInternalPlayer();
        const videoWidth = 1920 / 6;
        const videoHeight = 1080 / 6;
        setTimeout(() => {
          context.drawImage(video, 0, 0, videoWidth, videoHeight);
          setPreviewSrc(canvas.toDataURL());
        }, 100);
      }
      handlePosition(e);
    } catch (error) {
      console.error("Error generating preview:", error);
      return null;
    }
  };

  return (
    <div className="video-controls">
      <div className="slider-container">
        <progress className="seek-bar" max={1} value={loaded} />
        <input
          className="slider"
          type="range"
          min={0}
          max={0.999999}
          step="any"
          value={played}
          // onChange={(e) => onSeek(e, e.target.value)}
          onInput={(e) => onSeek(e, e.target.value)}
          onMouseUp={onSeekMouseUp}
          onMouseDown={onSeekMouseDown}
          onMouseMove={generatePreview}
          onMouseLeave={() => {
            setPosition(-100);
          }}
          onTouchStart={(e) => {
            e.preventDefault();
            onSeekMouseDown(e);
          }}
          onTouchEnd={(e) => {
            e.preventDefault();
            const newValue = e.target.value;
            onSeekMouseUp(e);
            setPosition(-100);
          }}
          onTouchMove={(e) => {
            e.preventDefault();
            generatePreview(e);
          }}
        />

        {previewSrc && position !== -100 && (
          <div className="preview-container" style={{ left: position }}>
            <Image
              priority={true}
              width={173}
              height={96.89}
              className="preview"
              src={previewSrc}
              alt="Preview"
            />
            <span class="preview_time">
              {parseDate(
                new Date(details?.occurrenceTime).getTime() +
                  previewVideoRef.current.getCurrentTime() * 1000
              )}
            </span>
          </div>
        )}
      </div>

      <Flex
        gap={0}
        align="center"
        justify="space-between"
        vertical={false}
        className="video-timer"
      >
        <div> {parseDate(details?.occurrenceTime)}</div>
        <div>
          <span className="time-spent">
            <Duration seconds={duration * played} />
          </span>
          <span className="dot" />
          <Duration seconds={duration} />
        </div>
        <div> {parseDate(details?.endTime)}</div>
      </Flex>
      <Flex gap={28} className="button-controls">
        <Image
          priority={true}
          onClick={() =>
            videoRef.current.seekTo(videoRef.current.getCurrentTime() - 5)
          }
          width={24}
          height={24}
          src="/svg/skip-video-back.svg"
          alt="skip-video-back-icon"
        />

        {!playing ? (
          <Image
            priority={true}
            width={24}
            height={24}
            src="/svg/play-video.svg"
            alt="play-icon"
            onClick={onPlayPause}
          />
        ) : (
          <Image
            priority={true}
            width={24}
            height={24}
            src="/svg/pause-video.svg"
            alt="pause-icon"
            onClick={onPlayPause}
          />
        )}

        <Image
          priority={true}
          onClick={() =>
            videoRef.current.seekTo(videoRef.current.getCurrentTime() + 5)
          }
          width={24}
          height={24}
          src="/svg/skip-video-forward.svg"
          alt="skip-forward-icon"
        />

        <Image
          priority={true}
          onClick={onRestart}
          width={24}
          height={24}
          src="/svg/restart-video.svg"
          alt="restart-icon"
        />
        <PlaybackSpeed
          playbackRate={playbackRate}
          onPlaybackRateChange={onPlaybackRateChange}
        />
      </Flex>
    </div>
  );
};

export default VideoControls;

i have tried using the ref values directly into these touch functions and also tried without the preventDefault function, but still it is not working

Upvotes: 0

Views: 108

Answers (0)

Related Questions