import { useEffect, useMemo, useRef, useState } from "react"; import { GroupProps, useThree } from "@react-three/fiber"; import Frame from "../misc/Frame"; import { DoubleSide, Material, PositionalAudio, sRGBEncoding, Vector2, AudioListener, } from "three"; type Props = { src: string; size?: number; framed?: boolean; muted?: boolean; volume?: number; frameMaterial?: Material; frameWidth?: number; } & GroupProps; export function Video(props: Props) { const { src, size = 1, framed, muted, volume = 1, frameMaterial, frameWidth = 1, ...rest } = props; const camera = useThree((state) => state.camera); const listener = useRef<THREE.AudioListener>(); const [speaker, setSpeaker] = useState<THREE.PositionalAudio>(); const [dims, setDims] = useState<Vector2 | null>(); const video = useMemo(() => { const v = document.createElement("video"); // @ts-ignore v.playsInline = true; v.crossOrigin = "Anonymous"; v.loop = true; v.src = src; v.autoplay = false; v.muted = muted ? muted : false; return v; }, []); useEffect(() => { const setupAudio = () => { if (!muted && !video.paused && !speaker) { const listener = new AudioListener(); camera.add(listener); const speak = new PositionalAudio(listener); speak.setMediaElementSource(video); speak.setRefDistance(0.75); speak.setRolloffFactor(1); speak.setVolume(volume); speak.setDirectionalCone(180, 230, 0.1); setSpeaker(speak); } }; const playVideo = () => { video .play() .then(() => setDims(new Vector2(video.videoWidth, video.videoHeight))); setupAudio(); }; if (video) { video.play().then(() => { setDims(new Vector2(video.videoWidth, video.videoHeight)); setupAudio(); }); document.addEventListener("click", playVideo); return () => { document.removeEventListener("click", playVideo); }; } }, [speaker, video, muted]); useEffect(() => { return () => { if (listener.current) { camera.remove(listener.current); listener.current.clear(); listener.current = undefined; } if (speaker) { speaker.clear(); speaker.disconnect(); setSpeaker(undefined); } if (video) { video.pause(); video.remove(); } }; }, []); if (!dims || !video) { return null; } const max = Math.max(dims.x, dims.y); const width = (dims.x / max) * size; const height = (dims.y / max) * size; return ( <group name="spacesvr-video" {...rest}> <mesh> <planeBufferGeometry attach="geometry" args={[width, height]} /> <meshBasicMaterial side={DoubleSide}> <videoTexture attach="map" args={[video]} encoding={sRGBEncoding} /> </meshBasicMaterial> </mesh> {speaker && <primitive object={speaker} />} {framed && ( <Frame width={width} height={height} thickness={frameWidth} material={frameMaterial} /> )} </group> ); }