Skip to content

Commit 20234b5

Browse files
authored
Merge pull request #65 from ADARSHsri2004/feat/preview
feat: added audio and video preview
2 parents 43a1393 + 82bba50 commit 20234b5

4 files changed

Lines changed: 336 additions & 232 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
3+
const PreJoinPreview: React.FC<{ onJoin: (stream: MediaStream) => void }> = ({ onJoin }) => {
4+
const videoRef = useRef<HTMLVideoElement>(null);
5+
const [stream, setStream] = useState<MediaStream | null>(null);
6+
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
7+
const [selectedCam, setSelectedCam] = useState<string>("");
8+
const [selectedMic, setSelectedMic] = useState<string>("");
9+
const [isMuted, setIsMuted] = useState(false);
10+
const [isCamOn, setIsCamOn] = useState(true);
11+
12+
// 🔹 Fetch devices + get default media
13+
useEffect(() => {
14+
const initMedia = async () => {
15+
try {
16+
const newStream = await navigator.mediaDevices.getUserMedia({
17+
video: true,
18+
audio: true,
19+
});
20+
setStream(newStream);
21+
if (videoRef.current) videoRef.current.srcObject = newStream;
22+
23+
const devicesList = await navigator.mediaDevices.enumerateDevices();
24+
setDevices(devicesList);
25+
} catch (err) {
26+
console.error("Media access denied:", err);
27+
}
28+
};
29+
30+
initMedia();
31+
32+
return () => {
33+
// 🔹 Cleanup
34+
stream?.getTracks().forEach(track => track.stop());
35+
};
36+
}, []);
37+
38+
const handleToggleMic = () => {
39+
if (stream) {
40+
stream.getAudioTracks().forEach(track => (track.enabled = !track.enabled));
41+
setIsMuted(prev => !prev);
42+
}
43+
};
44+
45+
const handleToggleCam = () => {
46+
if (stream) {
47+
stream.getVideoTracks().forEach(track => (track.enabled = !track.enabled));
48+
setIsCamOn(prev => !prev);
49+
}
50+
};
51+
52+
const handleDeviceChange = async (deviceId: string, type: "audioinput" | "videoinput") => {
53+
if (!stream) return;
54+
55+
// Stop old tracks of the same kind
56+
stream.getTracks()
57+
.filter(track => track.kind === (type === "audioinput" ? "audio" : "video"))
58+
.forEach(track => track.stop());
59+
60+
const constraints: MediaStreamConstraints =
61+
type === "audioinput"
62+
? { audio: { deviceId }, video: isCamOn }
63+
: { video: { deviceId }, audio: true };
64+
65+
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
66+
setStream(newStream);
67+
if (videoRef.current) videoRef.current.srcObject = newStream;
68+
69+
if (type === "videoinput") setSelectedCam(deviceId);
70+
else setSelectedMic(deviceId);
71+
};
72+
73+
return (
74+
<div className="flex flex-col items-center gap-4 p-6 bg-gray-900 text-white rounded-2xl shadow-lg">
75+
<h2 className="text-xl font-semibold">Preview Your Setup</h2>
76+
77+
<video
78+
ref={videoRef}
79+
autoPlay
80+
muted
81+
playsInline
82+
className="rounded-xl w-80 h-56 bg-black object-cover"
83+
/>
84+
85+
<div className="flex gap-4 mt-3">
86+
<button onClick={handleToggleCam} className="px-3 py-2 bg-blue-600 rounded-lg hover:bg-blue-700">
87+
{isCamOn ? "Turn Off Camera" : "Turn On Camera"}
88+
</button>
89+
<button onClick={handleToggleMic} className="px-3 py-2 bg-blue-600 rounded-lg hover:bg-blue-700">
90+
{isMuted ? "Unmute Mic" : "Mute Mic"}
91+
</button>
92+
</div>
93+
94+
<div className="flex flex-col gap-2 mt-4 w-80">
95+
<label>Camera:</label>
96+
<select
97+
value={selectedCam}
98+
onChange={e => handleDeviceChange(e.target.value, "videoinput")}
99+
className="text-black p-2 rounded-lg"
100+
>
101+
{devices
102+
.filter(d => d.kind === "videoinput")
103+
.map(cam => (
104+
<option key={cam.deviceId} value={cam.deviceId}>
105+
{cam.label || `Camera ${cam.deviceId.slice(0, 5)}`}
106+
</option>
107+
))}
108+
</select>
109+
110+
<label>Microphone:</label>
111+
<select
112+
value={selectedMic}
113+
onChange={e => handleDeviceChange(e.target.value, "audioinput")}
114+
className="text-black p-2 rounded-lg"
115+
>
116+
{devices
117+
.filter(d => d.kind === "audioinput")
118+
.map(mic => (
119+
<option key={mic.deviceId} value={mic.deviceId}>
120+
{mic.label || `Mic ${mic.deviceId.slice(0, 5)}`}
121+
</option>
122+
))}
123+
</select>
124+
</div>
125+
126+
<button
127+
onClick={() => onJoin(stream!)}
128+
className="mt-6 px-6 py-3 bg-green-600 rounded-xl hover:bg-green-700"
129+
>
130+
Join Call
131+
</button>
132+
</div>
133+
);
134+
};
135+
136+
export default PreJoinPreview;

0 commit comments

Comments
 (0)