我正在使用react-player库在Next JS中制作一个自定义视频播放器,除了正常的控件之外,我还在桌面上添加了悬停缩略图预览,并在触摸设备上添加了onTouchMove。控件在桌面上工作正常,但当我在 iOS 或 Android 设备上打开它时,视频播放器不会将视频搜索到触摸位置,并且在搜索栏上移动手指时,它只显示视频的预览和滑块不改变它的位置,相反,我想要的是,在视频播放期间,无论我将手指放在搜索栏上,视频都应该从那里开始播放
VideoPlayer 组件看起来像这样
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;
视频控件看起来像这样
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;
我尝试过直接在这些触摸函数中使用引用值,并且也尝试过不使用 PreventDefault 函数,但它仍然不起作用
使用
touches[0].clientX
进行触摸事件:
在
calcSliderPosition
中,不检查e.type,直接访问touches[0].clientX
来获取触摸事件:
const calcSliderPosition = (e) => {
let clientX;
if (e.type === "touchmove") {
clientX = e.touches[0].clientX;
} else {
clientX = e.nativeEvent.offsetX;
}
// ... rest of the function
}