Enhance Video and StreamPage components with improved media stream handling. Added audio and video track management, refined microphone and camera status updates, and optimized rendering based on available media streams. Improved user experience with conditional rendering and state management for audio and video devices.

This commit is contained in:
2026-02-27 18:34:51 +05:00
parent cb156bd99d
commit c2fc1624a4
3 changed files with 216 additions and 112 deletions
+68 -25
View File
@@ -13,51 +13,94 @@ interface Props {
user?: IUser; user?: IUser;
} }
const SPEAKING_HIDE_DELAY_MS = 400;
function Video({ mediaStream, muted, user }: Props) { function Video({ mediaStream, muted, user }: Props) {
const remoteVideoRef = useRef<HTMLVideoElement>(null); const remoteVideoRef = useRef<HTMLVideoElement>(null);
const remoteAudioRef = useRef<HTMLAudioElement>(null);
const isSpeaking = useIsAudioActive({ source: mediaStream }); const isSpeaking = useIsAudioActive({ source: mediaStream });
const [showSpeakingBorder, setShowSpeakingBorder] = useState(false);
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [_muted, setMuted] = useState(muted); const [_muted, setMuted] = useState(muted);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [minimized, setMinimized] = useState(user?.isAdmin ? false : true); const [minimized, setMinimized] = useState(user?.isAdmin ? false : true);
const hasVideo = (mediaStream?.getVideoTracks().length ?? 0) > 0;
useEffect(() => {
if (user && user.micEnabled === false) {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
setShowSpeakingBorder(false);
return;
}
if (isSpeaking) {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
setShowSpeakingBorder(true);
} else {
hideTimeoutRef.current = setTimeout(() => {
setShowSpeakingBorder(false);
hideTimeoutRef.current = null;
}, SPEAKING_HIDE_DELAY_MS);
}
return () => {
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
};
}, [isSpeaking, user?.micEnabled]);
function toggleSound() { function toggleSound() {
if (!remoteVideoRef.current) return;
// remoteVideoRef.current.muted = !remoteVideoRef.current.muted;
setMuted((prev) => !prev); setMuted((prev) => !prev);
} }
useEffect(() => { useEffect(() => {
if (!remoteVideoRef.current) return; if (!mediaStream) return;
remoteVideoRef.current.srcObject = mediaStream; if (hasVideo && remoteVideoRef.current) {
remoteVideoRef.current.onloadedmetadata = () => { remoteVideoRef.current.srcObject = mediaStream;
remoteVideoRef.current?.play(); remoteVideoRef.current.onloadedmetadata = () => {
}; remoteVideoRef.current?.play();
};
remoteVideoRef.current.onplay = () => { remoteVideoRef.current.onplay = () => setIsLoading(false);
} else if (!hasVideo && remoteAudioRef.current) {
remoteAudioRef.current.srcObject = mediaStream;
remoteAudioRef.current.onloadedmetadata = () => {
remoteAudioRef.current?.play();
};
remoteAudioRef.current.onplay = () => setIsLoading(false);
setIsLoading(false); setIsLoading(false);
}; }
}, [mediaStream, hasVideo]);
console.log("mediaStream", mediaStream?.getTracks());
}, [mediaStream]);
useEffect(() => {
console.log("remoteVideoRef.current", remoteVideoRef);
}, [remoteVideoRef.current]);
return ( return (
<div <div
className={`relative border-2 rounded-lg ${ className={`relative border-2 rounded-lg ${
minimized ? "h-8 rounded-lg overflow-hidden" : "" minimized ? "h-8 rounded-lg overflow-hidden" : ""
} ${!_muted && user?.micEnabled && isSpeaking ? "border-green-500" : "border-transparent"}`} } ${!_muted && user?.micEnabled !== false && showSpeakingBorder ? "border-green-500" : "border-transparent"}`}
> >
<video {hasVideo ? (
ref={remoteVideoRef} <video
className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500`} ref={remoteVideoRef}
playsInline className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500`}
autoPlay playsInline
muted={_muted} autoPlay
></video> muted={_muted}
></video>
) : (
<>
<audio
ref={remoteAudioRef}
autoPlay
muted={_muted}
className="hidden"
/>
<div className="aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg bg-gray-500" />
</>
)}
<div <div
className={`absolute -bottom-1.5 flex items-center justify-between w-full gap-2 p-2 ${ className={`absolute -bottom-1.5 flex items-center justify-between w-full gap-2 p-2 ${
minimized ? "bg-black" : "" minimized ? "bg-black" : ""
+30 -22
View File
@@ -49,52 +49,60 @@ function SetNameModal({ onAction }: Props) {
videoStreamRef.current = null; videoStreamRef.current = null;
try { try {
const constraints: MediaStreamConstraints = { const audioConstraints: MediaStreamConstraints = {
audio: audioDeviceId audio: audioDeviceId
? { deviceId: { exact: audioDeviceId } } ? { deviceId: { exact: audioDeviceId } }
: true, : true,
video: videoDeviceId
? { deviceId: { exact: videoDeviceId } }
: true,
}; };
const stream = await navigator.mediaDevices.getUserMedia(constraints); const audioStreamResult = await navigator.mediaDevices.getUserMedia(
audioConstraints
const devices = await navigator.mediaDevices.enumerateDevices(); );
const microphones = devices.filter((d) => d.kind === "audioinput"); const audioTracks = audioStreamResult.getAudioTracks();
const cameras = devices.filter((d) => d.kind === "videoinput");
const audioTracks = stream.getAudioTracks();
const videoTracks = stream.getVideoTracks();
if (audioTracks.length) { if (audioTracks.length) {
const audioMediaStream = new MediaStream(audioTracks); const audioMediaStream = new MediaStream(audioTracks);
audioStreamRef.current = audioMediaStream; audioStreamRef.current = audioMediaStream;
setAudioStream(audioMediaStream); setAudioStream(audioMediaStream);
setAudioDevices(microphones);
setSelectedAudioDeviceId(
audioDeviceId ?? microphones[0]?.deviceId ?? ""
);
setMicStatus("success"); setMicStatus("success");
} else { } else {
setMicStatus("error"); setMicStatus("error");
} }
} catch {
setMicStatus("error");
}
try {
const videoConstraints: MediaStreamConstraints = {
video: videoDeviceId
? { deviceId: { exact: videoDeviceId } }
: true,
};
const videoStreamResult = await navigator.mediaDevices.getUserMedia(
videoConstraints
);
const videoTracks = videoStreamResult.getVideoTracks();
if (videoTracks.length) { if (videoTracks.length) {
const videoMediaStream = new MediaStream(videoTracks); const videoMediaStream = new MediaStream(videoTracks);
videoStreamRef.current = videoMediaStream; videoStreamRef.current = videoMediaStream;
setVideoStream(videoMediaStream); setVideoStream(videoMediaStream);
setVideoDevices(cameras);
setSelectedVideoDeviceId(
videoDeviceId ?? cameras[0]?.deviceId ?? ""
);
setCameraStatus("success"); setCameraStatus("success");
} else { } else {
setCameraStatus("error"); setCameraStatus("error");
} }
} catch { } catch {
setMicStatus("error");
setCameraStatus("error"); setCameraStatus("error");
} }
const devices = await navigator.mediaDevices.enumerateDevices();
const microphones = devices.filter((d) => d.kind === "audioinput");
const cameras = devices.filter((d) => d.kind === "videoinput");
setAudioDevices(microphones);
setVideoDevices(cameras);
setSelectedAudioDeviceId(
audioDeviceId ?? microphones[0]?.deviceId ?? ""
);
setSelectedVideoDeviceId(
videoDeviceId ?? cameras[0]?.deviceId ?? ""
);
} }
async function checkMicrophone(deviceId?: string) { async function checkMicrophone(deviceId?: string) {
+118 -65
View File
@@ -147,25 +147,37 @@ function StreamPage() {
...existingVideoStream!.getVideoTracks(), ...existingVideoStream!.getVideoTracks(),
]); ]);
} else if (hasAudio) { } else if (hasAudio) {
const videoStream = await navigator.mediaDevices.getUserMedia({ try {
video: videoDeviceId const videoStream = await navigator.mediaDevices.getUserMedia({
? { deviceId: { exact: videoDeviceId } } video: videoDeviceId
: true, ? { deviceId: { exact: videoDeviceId } }
}); : true,
mediaStream = new MediaStream([ });
...existingAudioStream!.getAudioTracks(), mediaStream = new MediaStream([
...videoStream.getVideoTracks(), ...existingAudioStream!.getAudioTracks(),
]); ...videoStream.getVideoTracks(),
]);
} catch {
mediaStream = new MediaStream(
existingAudioStream!.getAudioTracks()
);
}
} else if (hasVideo) { } else if (hasVideo) {
const audioStream = await navigator.mediaDevices.getUserMedia({ try {
audio: audioDeviceId const audioStream = await navigator.mediaDevices.getUserMedia({
? { deviceId: { exact: audioDeviceId } } audio: audioDeviceId
: true, ? { deviceId: { exact: audioDeviceId } }
}); : true,
mediaStream = new MediaStream([ });
...audioStream.getAudioTracks(), mediaStream = new MediaStream([
...existingVideoStream!.getVideoTracks(), ...audioStream.getAudioTracks(),
]); ...existingVideoStream!.getVideoTracks(),
]);
} catch {
mediaStream = new MediaStream(
existingVideoStream!.getVideoTracks()
);
}
} else { } else {
mediaStream = await navigator.mediaDevices.getUserMedia({ mediaStream = await navigator.mediaDevices.getUserMedia({
video: videoDeviceId video: videoDeviceId
@@ -177,12 +189,15 @@ function StreamPage() {
}); });
} }
if (!localVideoRef.current) return; if (
localVideoRef.current &&
localVideoRef.current.srcObject = mediaStream; mediaStream.getVideoTracks().length > 0
localVideoRef.current.onloadedmetadata = () => { ) {
localVideoRef.current?.play(); localVideoRef.current.srcObject = mediaStream;
}; localVideoRef.current.onloadedmetadata = () => {
localVideoRef.current?.play();
};
}
setLocalStream(mediaStream); setLocalStream(mediaStream);
setPermission(true); setPermission(true);
@@ -479,6 +494,15 @@ function StreamPage() {
}, [users.length]); }, [users.length]);
useEffect(() => { useEffect(() => {
if (
localVideoRef.current &&
localStream.getVideoTracks().length > 0
) {
localVideoRef.current.srcObject = localStream;
localVideoRef.current.onloadedmetadata = () => {
localVideoRef.current?.play();
};
}
toggleCamera(); toggleCamera();
toggleMic(); toggleMic();
}, [localStream]); }, [localStream]);
@@ -564,44 +588,52 @@ function StreamPage() {
{permission && ( {permission && (
<> <>
<div className="relative group"> {localStream.getAudioTracks().length > 0 && (
<Button <div className="relative group">
variant="secondary" <Button
icon={ variant="secondary"
isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon /> icon={
} isMicEnabled ? (
onlyIcon <MicroOnIcon />
onClick={toggleMic} ) : (
/> <MicroOffIcon />
<Tooltip )
text={ }
isMicEnabled onlyIcon
? t("tooltips.turnOffMic") onClick={toggleMic}
: t("tooltips.turnOnMic") />
} <Tooltip
/> text={
</div> isMicEnabled
<div className="relative group"> ? t("tooltips.turnOffMic")
<Button : t("tooltips.turnOnMic")
variant="secondary" }
icon={ />
isCameraEnabled ? ( </div>
<CameraOnIcon /> )}
) : ( {localStream.getVideoTracks().length > 0 && (
<CameraOffIcon /> <div className="relative group">
) <Button
} variant="secondary"
onlyIcon icon={
onClick={toggleCamera} isCameraEnabled ? (
/> <CameraOnIcon />
<Tooltip ) : (
text={ <CameraOffIcon />
isCameraEnabled )
? t("tooltips.turnOffCamera") }
: t("tooltips.turnOnCamera") onlyIcon
} onClick={toggleCamera}
/> />
</div> <Tooltip
text={
isCameraEnabled
? t("tooltips.turnOffCamera")
: t("tooltips.turnOnCamera")
}
/>
</div>
)}
</> </>
)} )}
</div> </div>
@@ -733,7 +765,12 @@ function StreamPage() {
<div className="absolute top-2 space-y-2 lg:left-2 max-lg:right-2"> <div className="absolute top-2 space-y-2 lg:left-2 max-lg:right-2">
<div <div
className={`relative border-2 rounded-lg ${ className={`relative border-2 rounded-lg ${
!permission || !isCameraEnabled ? "hidden" : "" !permission ? "hidden" : ""
} ${
localStream.getVideoTracks().length === 0 ||
!isCameraEnabled
? "h-8 overflow-hidden"
: ""
} ${ } ${
isMicEnabled && isSpeaking isMicEnabled && isSpeaking
? "border-green-500" ? "border-green-500"
@@ -742,12 +779,28 @@ function StreamPage() {
> >
<video <video
ref={localVideoRef} ref={localVideoRef}
className={`object-cover bg-gray-500 rounded-lg aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] -scale-x-100`} className={`object-cover bg-gray-500 rounded-lg aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] -scale-x-100 ${
localStream.getVideoTracks().length === 0 ||
!isCameraEnabled
? "hidden"
: ""
}`}
playsInline playsInline
autoPlay autoPlay
muted muted
></video> ></video>
<div className="absolute bottom-0 p-2"> {(localStream.getVideoTracks().length === 0 ||
!isCameraEnabled) && (
<div className="aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg bg-gray-500" />
)}
<div
className={`absolute -bottom-1.5 flex items-center lg:w-[216px] w-[160px] gap-2 p-2 ${
localStream.getVideoTracks().length === 0 ||
!isCameraEnabled
? "bg-black"
: ""
}`}
>
<p className="text-xs text-white truncate lg:text-sm"> <p className="text-xs text-white truncate lg:text-sm">
{name} {name}
</p> </p>