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:
+68
-25
@@ -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" : ""
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user