2025-01-08 00:56:15 +08:00
|
|
|
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
|
|
import Hls from "hls.js";
|
|
|
|
import { VideoPlayerContext } from "./VideoPlayer";
|
|
|
|
|
2025-01-08 20:29:07 +08:00
|
|
|
interface VideoDisplayProps {
|
2025-01-08 00:56:15 +08:00
|
|
|
autoPlay?: boolean;
|
|
|
|
}
|
2025-01-08 20:29:07 +08:00
|
|
|
export const VideoDisplay: React.FC<VideoDisplayProps> = ({
|
2025-01-08 00:56:15 +08:00
|
|
|
autoPlay = false,
|
|
|
|
}) => {
|
|
|
|
const {
|
|
|
|
src,
|
|
|
|
poster,
|
|
|
|
onError,
|
|
|
|
videoRef,
|
|
|
|
setIsReady,
|
|
|
|
setIsPlaying,
|
|
|
|
setError,
|
|
|
|
setBufferingState,
|
|
|
|
isMuted,
|
|
|
|
setLoadingProgress,
|
|
|
|
setCurrentTime,
|
|
|
|
setDuration,
|
|
|
|
brightness,
|
|
|
|
isDragging,
|
|
|
|
setIsDragging,
|
|
|
|
progressRef,
|
2025-03-05 17:30:25 +08:00
|
|
|
isPlaying
|
2025-01-08 00:56:15 +08:00
|
|
|
} = useContext(VideoPlayerContext);
|
|
|
|
|
2025-01-08 20:29:07 +08:00
|
|
|
// 处理进度条拖拽
|
2025-01-08 00:56:15 +08:00
|
|
|
const handleProgressDrag = (e: MouseEvent) => {
|
|
|
|
if (!isDragging || !videoRef.current || !progressRef.current) return;
|
|
|
|
const rect = progressRef.current.getBoundingClientRect();
|
|
|
|
const percent = Math.max(
|
|
|
|
0,
|
|
|
|
Math.min(1, (e.clientX - rect.left) / rect.width)
|
|
|
|
);
|
|
|
|
videoRef.current.currentTime = percent * videoRef.current.duration;
|
|
|
|
};
|
|
|
|
// 添加拖拽事件监听
|
|
|
|
useEffect(() => {
|
|
|
|
const handleMouseUp = () => setIsDragging(false);
|
|
|
|
const handleMouseMove = (e: MouseEvent) => handleProgressDrag(e);
|
|
|
|
if (isDragging) {
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
|
|
}
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
|
|
};
|
|
|
|
}, [isDragging]);
|
|
|
|
|
2025-01-08 20:29:07 +08:00
|
|
|
// 初始化 HLS 和事件监听
|
2025-01-08 00:56:15 +08:00
|
|
|
useEffect(() => {
|
|
|
|
let hls: Hls;
|
|
|
|
const initializeHls = async () => {
|
|
|
|
if (!videoRef.current) return;
|
|
|
|
|
|
|
|
// Reset states
|
|
|
|
setIsReady(false);
|
|
|
|
setError(null);
|
|
|
|
setLoadingProgress(0);
|
|
|
|
setBufferingState(false);
|
|
|
|
// Check for native HLS support (Safari)
|
|
|
|
if (videoRef.current.canPlayType("application/vnd.apple.mpegurl")) {
|
|
|
|
videoRef.current.src = src;
|
|
|
|
setIsReady(true);
|
2025-01-08 20:29:07 +08:00
|
|
|
// 设置视频时长
|
|
|
|
setDuration(videoRef.current.duration);
|
2025-01-08 00:56:15 +08:00
|
|
|
if (autoPlay) {
|
|
|
|
try {
|
|
|
|
await videoRef.current.play();
|
|
|
|
setIsPlaying(true);
|
|
|
|
} catch (error) {
|
|
|
|
console.log("Auto-play prevented:", error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!Hls.isSupported()) {
|
|
|
|
const errorMessage = "您的浏览器不支持 HLS 视频播放";
|
|
|
|
setError(errorMessage);
|
|
|
|
onError?.(errorMessage);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
hls = new Hls({
|
|
|
|
maxBufferLength: 30,
|
|
|
|
maxMaxBufferLength: 600,
|
|
|
|
enableWorker: true,
|
|
|
|
debug: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
hls.loadSource(src);
|
|
|
|
hls.attachMedia(videoRef.current);
|
|
|
|
hls.on(Hls.Events.MANIFEST_PARSED, async () => {
|
|
|
|
setIsReady(true);
|
2025-01-08 20:29:07 +08:00
|
|
|
// 设置视频时长
|
|
|
|
setDuration(videoRef.current?.duration || 0);
|
2025-01-08 00:56:15 +08:00
|
|
|
if (autoPlay && videoRef.current) {
|
|
|
|
try {
|
|
|
|
await videoRef.current.play();
|
|
|
|
setIsPlaying(true);
|
|
|
|
} catch (error) {
|
|
|
|
console.log("Auto-play prevented:", error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
hls.on(Hls.Events.BUFFER_APPENDING, () => {
|
|
|
|
setBufferingState(true);
|
|
|
|
});
|
|
|
|
hls.on(Hls.Events.FRAG_BUFFERED, (_, data) => {
|
|
|
|
setBufferingState(false);
|
|
|
|
if (data.stats) {
|
|
|
|
const progress =
|
|
|
|
(data.stats.loaded / data.stats.total) * 100;
|
|
|
|
setLoadingProgress(Math.round(progress));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
let fatalError;
|
2025-01-08 20:29:07 +08:00
|
|
|
let networkError;
|
2025-01-08 00:56:15 +08:00
|
|
|
hls.on(Hls.Events.ERROR, (_, data) => {
|
|
|
|
if (data.fatal) {
|
|
|
|
switch (data.type) {
|
|
|
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
|
|
networkError = `网络错误: ${data.details}`;
|
|
|
|
console.error(networkError);
|
|
|
|
setError(networkError);
|
|
|
|
onError?.(networkError);
|
|
|
|
hls.startLoad();
|
|
|
|
break;
|
|
|
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
|
|
console.error("Media error, attempting to recover");
|
|
|
|
setError("视频解码错误,尝试恢复...");
|
|
|
|
hls.recoverMediaError();
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
fatalError = `加载失败: ${data.details}`;
|
|
|
|
console.error(fatalError);
|
|
|
|
setError(fatalError);
|
|
|
|
onError?.(fatalError);
|
|
|
|
hls.destroy();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2025-01-08 20:29:07 +08:00
|
|
|
|
|
|
|
// 事件处理
|
2025-01-08 00:56:15 +08:00
|
|
|
const handlePlay = () => setIsPlaying(true);
|
|
|
|
const handlePause = () => setIsPlaying(false);
|
|
|
|
const handleEnded = () => setIsPlaying(false);
|
|
|
|
const handleWaiting = () => setBufferingState(true);
|
|
|
|
const handlePlaying = () => setBufferingState(false);
|
2025-01-08 20:29:07 +08:00
|
|
|
const handleLoadedMetadata = () => {
|
|
|
|
if (videoRef.current) {
|
|
|
|
// 设置视频时长
|
|
|
|
setDuration(videoRef.current.duration);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-01-08 00:56:15 +08:00
|
|
|
if (videoRef.current) {
|
|
|
|
videoRef.current.addEventListener("play", handlePlay);
|
|
|
|
videoRef.current.addEventListener("pause", handlePause);
|
|
|
|
videoRef.current.addEventListener("ended", handleEnded);
|
|
|
|
videoRef.current.addEventListener("waiting", handleWaiting);
|
|
|
|
videoRef.current.addEventListener("playing", handlePlaying);
|
2025-01-08 20:29:07 +08:00
|
|
|
videoRef.current.addEventListener(
|
|
|
|
"loadedmetadata",
|
|
|
|
handleLoadedMetadata
|
|
|
|
);
|
2025-01-08 00:56:15 +08:00
|
|
|
}
|
2025-01-08 20:29:07 +08:00
|
|
|
|
2025-01-08 00:56:15 +08:00
|
|
|
initializeHls();
|
2025-01-08 20:29:07 +08:00
|
|
|
|
2025-01-08 00:56:15 +08:00
|
|
|
return () => {
|
|
|
|
if (videoRef.current) {
|
|
|
|
videoRef.current.removeEventListener("play", handlePlay);
|
|
|
|
videoRef.current.removeEventListener("pause", handlePause);
|
|
|
|
videoRef.current.removeEventListener("ended", handleEnded);
|
|
|
|
videoRef.current.removeEventListener("waiting", handleWaiting);
|
|
|
|
videoRef.current.removeEventListener("playing", handlePlaying);
|
2025-01-08 20:29:07 +08:00
|
|
|
videoRef.current.removeEventListener(
|
|
|
|
"loadedmetadata",
|
|
|
|
handleLoadedMetadata
|
|
|
|
);
|
2025-01-08 00:56:15 +08:00
|
|
|
}
|
|
|
|
if (hls) {
|
|
|
|
hls.destroy();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, [src, onError, autoPlay]);
|
|
|
|
|
2025-03-05 16:50:47 +08:00
|
|
|
const handleVideoClick = () => {
|
2025-03-05 17:30:25 +08:00
|
|
|
if (videoRef.current && isPlaying) {
|
2025-03-05 16:50:47 +08:00
|
|
|
videoRef.current.pause();
|
|
|
|
setIsPlaying(false);
|
2025-03-05 17:30:25 +08:00
|
|
|
}else if (videoRef.current && !isPlaying) {
|
|
|
|
videoRef.current.play();
|
|
|
|
setIsPlaying(true);
|
2025-03-05 16:50:47 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-01-08 00:56:15 +08:00
|
|
|
return (
|
2025-03-05 17:30:25 +08:00
|
|
|
<div className="relative w-full aspect-video" >
|
2025-02-23 21:42:47 +08:00
|
|
|
<video
|
2025-03-05 17:30:25 +08:00
|
|
|
onClick={handleVideoClick}
|
2025-02-23 21:42:47 +08:00
|
|
|
ref={videoRef}
|
|
|
|
className="w-full h-full"
|
|
|
|
poster={poster}
|
|
|
|
controls={false}
|
|
|
|
playsInline
|
|
|
|
muted={isMuted}
|
|
|
|
style={{ filter: `brightness(${brightness})` }}
|
|
|
|
onTimeUpdate={() => {
|
|
|
|
if (videoRef.current) {
|
|
|
|
setCurrentTime(videoRef.current.currentTime);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
2025-01-08 00:56:15 +08:00
|
|
|
);
|
|
|
|
};
|