一个简单的视频播放控件,会在QQ浏览器上使用canvas进行视频播放
import {
CSSProperties,
ReactNode,
Ref,
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import videojs from "video.js";
import { fitSize } from "../theme";
interface SimpleVideoProps {
uri: string;
isPaused?: boolean;
muted?: boolean;
onVideoEnd?: () => void;
loop?: boolean;
poster?: string | undefined;
style?: CSSProperties | undefined;
w?: number | string | undefined;
h?: number | string | undefined;
children?: ReactNode;
isReset?: boolean | undefined;
resizeMode?: "cover" | "contain" | "stretch" | undefined;
}
interface SimpleVideoPropsRef {
toggleVideo?: () => void;
sonVideoPalyState?: boolean;
}
export const isQQ =
navigator.userAgent.indexOf("MQQBrowser") > -1 ||
navigator.userAgent.indexOf("QQTheme") > -1;
export const WebVideoCompatibility = forwardRef(
(props: SimpleVideoProps, ref: Ref<SimpleVideoPropsRef>) => {
return isQQ ? (
<CanvasVideo {...props} ref={ref} />
) : (
<WebVideo {...props} ref={ref} />
);
}
);
export const WebVideo = forwardRef(
(props: SimpleVideoProps, ref: Ref<SimpleVideoPropsRef>) => {
const {
uri,
isPaused = true,
muted = true,
loop = false,
poster,
children,
isReset = true,
w = "100%",
h = "100%",
style,
onVideoEnd,
} = props;
const videoRef = useRef<HTMLVideoElement | null>(null);
const playerRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(!isPaused);
const loadVideo = useCallback(async () => {
if (!uri) return;
try {
const videoElement = videoRef.current;
const player = videojs(videoElement, {
controls: false,
poster: poster ?? "",
autoplay: false,
muted: muted,
loop,
playsinline: true,
preload: "auto",
controlsList: "nodownload nofullscreen noremoteplayback",
html5: {
hls: {
enableLowInitialPlaylist: true,
smoothQualityChange: true,
},
},
plugins: {},
errorDisplay: false,
});
player.src(encodeURI(uri));
playerRef.current = player;
player.on("ended", () => {
onVideoEnd();
});
} catch (error) {
console.error("视频加载失败:", error);
return null;
}
}, [loop, muted, onVideoEnd, poster, uri]);
useEffect(() => {
if (!!uri) {
loadVideo();
}
return () => {
if (playerRef.current) {
playerRef.current.dispose();
}
};
}, [uri, loop, poster, loadVideo]);
const videoState = useCallback(() => {
const player = playerRef.current;
if (player) {
if (isPaused) {
player.pause();
if (isReset) player.currentTime(0);
} else {
player.play().catch((error) => {
console.error("Error playing video:", error);
});
}
}
}, [isPaused, isReset]);
useEffect(() => {
if (!!uri) {
videoState();
}
}, [isPaused, uri, videoState]);
useImperativeHandle(ref, () => ({
toggleVideo: () => {
if (playerRef.current) {
if (playerRef.current.paused()) {
playerRef.current.play();
setIsPlaying(true);
} else {
playerRef.current.pause();
setIsPlaying(false);
}
}
},
sonVideoPalyState: isPlaying,
}));
useEffect(() => {
const player = playerRef.current;
if (player) {
player.muted(muted);
}
}, [muted]);
const container: CSSProperties = {
width: w ?? "100%",
height: h ?? "100%",
backgroundColor: "transparent",
display: "flex",
alignItems: "center",
justifyContent: "center",
...style,
};
const content: CSSProperties = {
width: "100%",
height: fitSize(442),
display: "flex",
alignItems: "center",
justifyContent: "center",
};
const videoStyle: CSSProperties = {
objectFit: "cover",
width: "100%",
height: "100%",
};
return (
<div style={container}>
<div style={content}>
<video
className="video-js"
controls={false}
x5-video-player-type="h5-page"
muted={muted}
style={videoStyle}
ref={videoRef}
onPause={() => {
setIsPlaying(false);
}}
id="capsule"
/>
</div>
{children}
</div>
);
}
);
export const CanvasVideo = forwardRef(
(props: SimpleVideoProps, ref: Ref<SimpleVideoPropsRef>) => {
const {
uri,
isPaused = true,
loop = false,
style,
children,
isReset,
w = "100%",
h = "100%",
muted,
onVideoEnd,
} = props;
const canvasRef = useRef(null);
const videoRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
const video = videoRef.current;
video.addEventListener("play", () => {
const drawFrame = () => {
const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const scaleX = canvasWidth / videoWidth;
const scaleY = canvasHeight / videoHeight;
const scale = Math.max(scaleX, scaleY);
const drawWidth = videoWidth * scale;
const drawHeight = videoHeight * scale;
const xOffset = (drawWidth - canvasWidth) / 2;
const yOffset = (drawHeight - canvasHeight) / 2;
context.drawImage(video, -xOffset, -yOffset, drawWidth, drawHeight);
requestAnimationFrame(drawFrame);
};
drawFrame();
});
return () => {
video.removeEventListener("play", () => {
console.log("清理");
});
};
}, []);
useEffect(() => {
const video = videoRef.current;
if (video) {
if (isPaused) {
video.pause();
if (isReset) {
video.currentTime = 0;
}
} else {
video.play();
}
}
}, [isPaused, isReset]);
useImperativeHandle(ref, () => ({
toggleVideo: () => {
const video = videoRef.current;
if (video) {
if (video.paused) {
video.play();
} else {
video.pause();
}
}
},
}));
const absoluteStyle: CSSProperties = children
? { position: "absolute", top: 0, left: 0 }
: {};
const container: CSSProperties = {
width: "100%",
height: "100%",
backgroundColor: "transparent",
display: "flex",
alignItems: "center",
justifyContent: "center",
...style,
};
const videoStyle = { display: "none" };
const handleVideoEnded = useCallback(() => {
onVideoEnd();
}, [onVideoEnd]);
useEffect(() => {
const video = videoRef.current;
if (video) {
video.addEventListener("ended", handleVideoEnded);
return () => {
video.removeEventListener("ended", handleVideoEnded);
};
}
}, [handleVideoEnded]);
return (
<div style={container}>
<video
src={uri}
style={videoStyle}
ref={videoRef}
muted={muted}
loop={loop}
controls={false}
/>
<canvas ref={canvasRef} width={w} height={h} style={absoluteStyle} />
{children}
</div>
);
}
);