Reputation: 1
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