Skip to content

Commit d862d98

Browse files
authored
Merge pull request #75 from ADARSHsri2004/feat/error_device_permission
fea: added device permission error handling
2 parents 75819d0 + 59f87a6 commit d862d98

File tree

3 files changed

+218
-28
lines changed

3 files changed

+218
-28
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React from "react";
2+
import { motion, AnimatePresence } from "framer-motion";
3+
import { AlertTriangle, RefreshCw, X } from "lucide-react";
4+
5+
interface PermissionErrorModalProps {
6+
isOpen: boolean;
7+
message: string;
8+
onRetry: () => void;
9+
onClose: () => void;
10+
}
11+
12+
const PermissionErrorModal: React.FC<PermissionErrorModalProps> = ({
13+
isOpen,
14+
message,
15+
onRetry,
16+
onClose,
17+
}) => {
18+
return (
19+
<AnimatePresence>
20+
{isOpen && (
21+
<motion.div
22+
className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-9999"
23+
initial={{ opacity: 0 }}
24+
animate={{ opacity: 1 }}
25+
exit={{ opacity: 0 }}
26+
>
27+
{/* Modal Card */}
28+
<motion.div
29+
initial={{ scale: 0.85, opacity: 0 }}
30+
animate={{ scale: 1, opacity: 1 }}
31+
exit={{ scale: 0.8, opacity: 0 }}
32+
transition={{ type: "spring", damping: 14 }}
33+
className="bg-white rounded-2xl shadow-xl p-6 w-[90%] max-w-md"
34+
>
35+
{/* Header */}
36+
<div className="flex justify-between items-center mb-3">
37+
<div className="flex items-center gap-2 text-red-600 font-semibold text-lg">
38+
<AlertTriangle size={22} />
39+
Device Permission Error
40+
</div>
41+
<button onClick={onClose}>
42+
<X size={22} className="text-gray-500 hover:text-gray-700" />
43+
</button>
44+
</div>
45+
46+
{/* Message */}
47+
<p className="text-gray-800 leading-relaxed">{message}</p>
48+
49+
{/* Actions */}
50+
<div className="mt-5 flex flex-col gap-3">
51+
52+
{/* Retry Button */}
53+
<button
54+
onClick={onRetry}
55+
className="flex items-center justify-center gap-2 bg-red-600 hover:bg-red-700 text-white py-2.5 rounded-xl font-medium transition-all"
56+
>
57+
<RefreshCw size={18} />
58+
Retry Access
59+
</button>
60+
61+
{/* Browser Permission Links */}
62+
<div className="text-sm text-gray-700">
63+
<p className="font-semibold mb-1">Grant permissions in browser:</p>
64+
65+
<a
66+
href="chrome://settings/content/camera"
67+
target="_blank"
68+
className="block underline text-blue-600"
69+
>
70+
Open Camera Permissions
71+
</a>
72+
73+
<a
74+
href="chrome://settings/content/microphone"
75+
target="_blank"
76+
className="block underline text-blue-600 mt-1"
77+
>
78+
Open Microphone Permissions
79+
</a>
80+
</div>
81+
</div>
82+
</motion.div>
83+
</motion.div>
84+
)}
85+
</AnimatePresence>
86+
);
87+
};
88+
89+
export default PermissionErrorModal;

frontend/src/pages/InRoom.tsx

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ interface ChatMessage {
2222
const InRoom: React.FC = () => {
2323
const { roomName } = useParams<{ roomName: string }>();
2424
const navigate = useNavigate();
25+
const [permissionError, setPermissionError] = useState<string | null>(null);
26+
const [showPermissionModal, setShowPermissionModal] = useState(false);
27+
2528

2629
const [micOn, setMicOn] = useState(true);
2730
const [videoOn, setVideoOn] = useState(true);
@@ -233,32 +236,50 @@ const InRoom: React.FC = () => {
233236
}, [roomName, userName]);
234237

235238
// Initialize media stream
236-
useEffect(() => {
237-
const initMedia = async () => {
238-
try {
239-
const stream = await navigator.mediaDevices.getUserMedia({
240-
video: true,
241-
audio: true,
242-
});
243-
mediaStreamRef.current = stream;
244-
if (localVideoRef.current) localVideoRef.current.srcObject = stream;
245-
setMicOn(stream.getAudioTracks().some((t) => t.enabled));
246-
setVideoOn(stream.getVideoTracks().some((t) => t.enabled));
247-
} catch (err) {
248-
console.error("Error accessing camera/mic:", err);
249-
alert("Please allow camera and microphone permissions.");
250-
setMicOn(false);
251-
setVideoOn(false);
239+
const initMedia = async () => {
240+
try {
241+
const stream = await navigator.mediaDevices.getUserMedia({
242+
audio: true,
243+
video: true,
244+
});
245+
246+
// Success
247+
setPermissionError(null);
248+
setShowPermissionModal(false);
249+
250+
// Save stream to your refs/states
251+
mediaStreamRef.current = stream;
252+
setMicOn(true);
253+
setVideoOn(true);
254+
255+
// Attach tracks, send to peer, whatever your flow is
256+
if (localVideoRef.current) {
257+
localVideoRef.current.srcObject = stream;
252258
}
253-
};
254259

255-
initMedia();
260+
} catch (err: any) {
261+
console.error("Error accessing camera/mic:", err);
256262

257-
return () => {
258-
mediaStreamRef.current?.getTracks().forEach((track) => track.stop());
259-
};
263+
let msg = "Camera/Microphone access was denied. Please allow permissions.";
264+
265+
if (err.name === "NotAllowedError") {
266+
msg = "You blocked camera/mic access for this site. Please enable it from browser settings.";
267+
} else if (err.name === "NotFoundError") {
268+
msg = "No camera or microphone was found on your device.";
269+
}
270+
271+
setPermissionError(msg);
272+
setShowPermissionModal(true);
273+
274+
setMicOn(false);
275+
setVideoOn(false);
276+
}
277+
};
278+
useEffect(() => {
279+
initMedia();
260280
}, []);
261281

282+
262283
// When local media becomes available and a peer is present, ensure tracks are added
263284
useEffect(() => {
264285
// If we have a pc and a media stream, make sure tracks are added
@@ -452,6 +473,55 @@ const InRoom: React.FC = () => {
452473
</div>
453474
);
454475
}
476+
const permissionModal = showPermissionModal && (
477+
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50">
478+
<motion.div
479+
initial={{ scale: 0.9, opacity: 0 }}
480+
animate={{ scale: 1, opacity: 1 }}
481+
exit={{ scale: 0.9, opacity: 0 }}
482+
className="bg-gray-900 text-white w-full max-w-md p-6 rounded-2xl shadow-xl border border-gray-700"
483+
>
484+
<h2 className="text-xl font-semibold mb-3">Permissions Required</h2>
485+
486+
<p className="text-gray-300 mb-4 text-sm leading-relaxed">
487+
{permissionError}
488+
</p>
489+
490+
{/* Instructions when user BLOCKED permissions */}
491+
{permissionError?.includes("blocked") && (
492+
<div className="text-sm text-gray-400 mb-4">
493+
<p className="mb-2">To enable camera/mic permissions:</p>
494+
<ul className="list-disc ml-5 space-y-1">
495+
<li>Click the lock icon in the URL bar</li>
496+
<li>Open "Site Settings"</li>
497+
<li>Set Camera and Microphone to "Allow"</li>
498+
<li>Reload the page</li>
499+
</ul>
500+
</div>
501+
)}
502+
503+
<div className="flex justify-end gap-3 mt-6">
504+
<button
505+
onClick={() => setShowPermissionModal(false)}
506+
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-sm"
507+
>
508+
Close
509+
</button>
510+
511+
<button
512+
onClick={() => {
513+
setShowPermissionModal(false);
514+
initMedia(); // 🔥 This re-triggers browser permission prompt
515+
}}
516+
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 rounded-lg text-sm font-medium"
517+
>
518+
Retry
519+
</button>
520+
</div>
521+
</motion.div>
522+
</div>
523+
);
524+
455525

456526
return (
457527
<HotKeys keyMap={keyMap} handlers={handlers}>
@@ -477,6 +547,8 @@ const InRoom: React.FC = () => {
477547
</header>
478548

479549
{/* Main Video Area */}
550+
{permissionModal}
551+
480552
<div className="flex-1 flex flex-col md:flex-row">
481553
{/* Video Grid */}
482554
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-4 p-4">

frontend/src/pages/JoinRoom.tsx

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,40 @@ export default function JoinRoom() {
1414
const API_BASE = "http://localhost:3000/api/rooms";
1515

1616
// 🔹 Step 1: Handle room join initiation
17-
const handleJoinClick = (e: React.FormEvent) => {
17+
const handleJoinClick = async (e: React.FormEvent) => {
1818
e.preventDefault();
19+
1920
if (!roomName.trim()) return alert("Please enter a room name!");
20-
// Instead of joining immediately, show preview first
21-
setShowPreview(true);
21+
22+
// ⬇️ PREVIEW SHOULD ONLY SHOW IF MEDIA PERMISSION IS SUCCESSFUL
23+
try {
24+
const tempStream = await navigator.mediaDevices.getUserMedia({
25+
audio: true,
26+
video: true,
27+
});
28+
29+
setStream(tempStream);
30+
setShowPreview(true);
31+
} catch (err: any) {
32+
console.error("Media access denied:", err);
33+
34+
if (err.name === "NotAllowedError") {
35+
alert(
36+
"You blocked the camera/mic.\n\nPlease enable permissions:\n1. Click lock icon in URL bar\n2. Open Site Settings\n3. Set Camera & Microphone to Allow\n4. Reload the page"
37+
);
38+
} else {
39+
alert("Unable to access camera/mic: " + err.message);
40+
}
41+
}
2242
};
2343

2444
// 🔹 Step 2: After preview confirmation, actually join the backend room
2545
const handleConfirmJoin = async (mediaStream: MediaStream) => {
46+
if (!mediaStream) {
47+
alert("Camera or microphone not available!");
48+
return;
49+
}
50+
2651
setStream(mediaStream);
2752
setLoading(true);
2853

@@ -39,8 +64,10 @@ export default function JoinRoom() {
3964
);
4065

4166
alert("Joined room successfully!");
42-
// Optional: Stop preview stream before entering actual call
43-
mediaStream.getTracks().forEach(track => track.stop());
67+
68+
// 🛑 SAFE STOP (prevents getTracks() crash)
69+
mediaStream.getTracks().forEach((track) => track.stop());
70+
4471
navigate(`/room/${roomName}`);
4572
} catch (err: any) {
4673
console.error("Join room error:", err);
@@ -52,10 +79,10 @@ export default function JoinRoom() {
5279
};
5380

5481
// 🔹 Step 3: Conditional render — preview or join form
55-
if (showPreview) {
82+
if (showPreview && stream) {
5683
return (
5784
<div className="min-h-screen flex items-center justify-center bg-gray-900 dark:bg-gray-950">
58-
<PreJoinPreview onJoin={handleConfirmJoin} />
85+
<PreJoinPreview onJoin={handleConfirmJoin} initialStream={stream} />
5986
</div>
6087
);
6188
}
@@ -67,6 +94,7 @@ export default function JoinRoom() {
6794
<h2 className="text-3xl font-bold text-green-600 dark:text-green-500 text-center mb-6">
6895
Join a Room
6996
</h2>
97+
7098
<form onSubmit={handleJoinClick} className="space-y-4">
7199
<input
72100
type="text"
@@ -75,6 +103,7 @@ export default function JoinRoom() {
75103
placeholder="Enter room name"
76104
className="w-full border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-400 dark:focus:ring-green-500"
77105
/>
106+
78107
<Button
79108
type="submit"
80109
disabled={loading}

0 commit comments

Comments
 (0)