449 líneas
18 KiB
JavaScript
449 líneas
18 KiB
JavaScript
import { useSocket } from '@/context/SocketProvider';
|
|
import { useRouter } from 'next/router';
|
|
import React, { useCallback, useEffect, useState } from 'react'
|
|
import peer from '@/service/peer';
|
|
import { motion } from 'framer-motion';
|
|
import { Video, Phone, Users, Send, ArrowLeft } from 'lucide-react';
|
|
import VideoPlayer from '@/components/VideoPlayer';
|
|
import CallHandleButtons from '@/components/CallHandleButtons';
|
|
|
|
const RoomPage = () => {
|
|
const socket = useSocket();
|
|
const router = useRouter();
|
|
const { slug } = router.query;
|
|
|
|
const [remoteSocketId, setRemoteSocketId] = useState(null);
|
|
const [myStream, setMyStream] = useState(null);
|
|
const [remoteStream, setRemoteStream] = useState(null);
|
|
const [isAudioMute, setIsAudioMute] = useState(false);
|
|
const [isVideoOnHold, setIsVideoOnHold] = useState(false);
|
|
const [callButton, setCallButton] = useState(true);
|
|
const [isSendButtonVisible, setIsSendButtonVisible] = useState(true);
|
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
const [hasJoinedRoom, setHasJoinedRoom] = useState(false);
|
|
|
|
// Check if user came from lobby (has proper session) or direct navigation
|
|
useEffect(() => {
|
|
// Check if user has a valid session or email stored (you might want to implement proper session management)
|
|
const hasValidSession = localStorage.getItem('userEmail') || document.referrer.includes('/');
|
|
|
|
if (!hasValidSession && typeof window !== 'undefined') {
|
|
// User navigated directly to room page without going through lobby
|
|
console.warn('Direct navigation to room detected, redirecting to lobby');
|
|
router.push('/');
|
|
return;
|
|
}
|
|
|
|
// If user has a session, mark as joined
|
|
setHasJoinedRoom(true);
|
|
}, [router]);
|
|
|
|
// Store user email when they join from lobby
|
|
useEffect(() => {
|
|
socket.on("room:join", (data) => {
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem('userEmail', data.email);
|
|
localStorage.setItem('currentRoom', data.room);
|
|
}
|
|
});
|
|
|
|
// Listen for socket errors and provide user feedback
|
|
socket.on("error", (error) => {
|
|
console.error('Socket error:', error);
|
|
alert(`Connection Error: ${error.message || 'Something went wrong. Please try again.'}`);
|
|
// Redirect to lobby on error
|
|
router.push('/');
|
|
});
|
|
|
|
return () => {
|
|
socket.off("room:join");
|
|
socket.off("error");
|
|
};
|
|
}, [socket, router]);
|
|
|
|
const handleUserJoined = useCallback(({ email, id }) => {
|
|
console.log(`User ${email} joined the room!`);
|
|
setRemoteSocketId(id);
|
|
}, []);
|
|
|
|
const handleUserLeft = useCallback(({ email }) => {
|
|
console.log(`User ${email} left the room`);
|
|
setRemoteSocketId(null);
|
|
setRemoteStream(null);
|
|
}, []);
|
|
|
|
const handleIncomingCall = useCallback(async ({ from, offer }) => {
|
|
setRemoteSocketId(from);
|
|
setIsConnecting(true);
|
|
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
audio: true,
|
|
video: true
|
|
});
|
|
setMyStream(stream);
|
|
|
|
const ans = await peer.getAnswer(offer);
|
|
socket.emit("call:accepted", { to: from, ans });
|
|
} catch (error) {
|
|
console.error('Error handling incoming call:', error);
|
|
setIsConnecting(false);
|
|
}
|
|
}, [socket]);
|
|
|
|
const sendStreams = useCallback(() => {
|
|
if (myStream) {
|
|
for (const track of myStream.getTracks()) {
|
|
peer.peer.addTrack(track, myStream);
|
|
}
|
|
setIsSendButtonVisible(false);
|
|
}
|
|
}, [myStream]);
|
|
|
|
const handleCallAccepted = useCallback(({ from, ans }) => {
|
|
peer.setLocalDescription(ans);
|
|
console.log("Call Accepted");
|
|
setIsConnecting(false);
|
|
sendStreams();
|
|
}, [sendStreams]);
|
|
|
|
const handleNegoNeededIncoming = useCallback(async ({ from, offer }) => {
|
|
const ans = await peer.getAnswer(offer);
|
|
socket.emit("peer:nego:done", { to: from, ans });
|
|
}, [socket]);
|
|
|
|
const handleNegoNeeded = useCallback(async () => {
|
|
const offer = await peer.getOffer();
|
|
socket.emit("peer:nego:needed", { offer, to: remoteSocketId });
|
|
}, [remoteSocketId, socket]);
|
|
|
|
const handleNegoFinal = useCallback(async ({ ans }) => {
|
|
await peer.setLocalDescription(ans);
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
peer.peer.addEventListener('negotiationneeded', handleNegoNeeded);
|
|
return () => {
|
|
peer.peer.removeEventListener('negotiationneeded', handleNegoNeeded);
|
|
}
|
|
}, [handleNegoNeeded]);
|
|
|
|
useEffect(() => {
|
|
peer.peer.addEventListener('track', async ev => {
|
|
const remoteStream = ev.streams;
|
|
console.log("GOT TRACKS!");
|
|
setRemoteStream(remoteStream[0]);
|
|
setIsConnecting(false);
|
|
})
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
socket.on("user:joined", handleUserJoined);
|
|
socket.on("user:left", handleUserLeft);
|
|
socket.on("incoming:call", handleIncomingCall);
|
|
socket.on("call:accepted", handleCallAccepted);
|
|
socket.on("peer:nego:needed", handleNegoNeededIncoming);
|
|
socket.on("peer:nego:final", handleNegoFinal);
|
|
|
|
return () => {
|
|
socket.off("user:joined", handleUserJoined);
|
|
socket.off("user:left", handleUserLeft);
|
|
socket.off("incoming:call", handleIncomingCall);
|
|
socket.off("call:accepted", handleCallAccepted);
|
|
socket.off("peer:nego:needed", handleNegoNeededIncoming);
|
|
socket.off("peer:nego:final", handleNegoFinal);
|
|
};
|
|
}, [
|
|
socket,
|
|
handleUserJoined,
|
|
handleUserLeft,
|
|
handleIncomingCall,
|
|
handleCallAccepted,
|
|
handleNegoNeededIncoming,
|
|
handleNegoFinal
|
|
]);
|
|
|
|
useEffect(() => {
|
|
socket.on("call:end", ({ from }) => {
|
|
if (from === remoteSocketId) {
|
|
peer.peer.close();
|
|
|
|
if (myStream) {
|
|
myStream.getTracks().forEach(track => track.stop());
|
|
setMyStream(null);
|
|
}
|
|
|
|
setRemoteStream(null);
|
|
setRemoteSocketId(null);
|
|
setCallButton(true);
|
|
setIsSendButtonVisible(true);
|
|
setIsConnecting(false);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
socket.off("call:end");
|
|
}
|
|
}, [remoteSocketId, myStream, socket]);
|
|
|
|
useEffect(() => {
|
|
socket.on("call:initiated", ({ from }) => {
|
|
if (from === remoteSocketId) {
|
|
setCallButton(false);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
socket.off("call:initiated");
|
|
}
|
|
}, [socket, remoteSocketId]);
|
|
|
|
const handleCallUser = useCallback(async () => {
|
|
setIsConnecting(true);
|
|
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
audio: true,
|
|
video: true
|
|
});
|
|
|
|
if (isAudioMute) {
|
|
const audioTracks = stream.getAudioTracks();
|
|
audioTracks.forEach(track => track.enabled = false);
|
|
}
|
|
|
|
if (isVideoOnHold) {
|
|
const videoTracks = stream.getVideoTracks();
|
|
videoTracks.forEach(track => track.enabled = false);
|
|
}
|
|
|
|
const offer = await peer.getOffer();
|
|
socket.emit("user:call", { to: remoteSocketId, offer })
|
|
setMyStream(stream);
|
|
setCallButton(false);
|
|
socket.emit("call:initiated", { to: remoteSocketId });
|
|
} catch (error) {
|
|
console.error('Error starting call:', error);
|
|
setIsConnecting(false);
|
|
}
|
|
}, [remoteSocketId, socket, isAudioMute, isVideoOnHold]);
|
|
|
|
const handleToggleAudio = () => {
|
|
if (myStream) {
|
|
const audioTracks = myStream.getAudioTracks();
|
|
audioTracks.forEach(track => track.enabled = !track.enabled);
|
|
setIsAudioMute(!isAudioMute);
|
|
}
|
|
};
|
|
|
|
const handleToggleVideo = () => {
|
|
if (myStream) {
|
|
const videoTracks = myStream.getVideoTracks();
|
|
videoTracks.forEach(track => track.enabled = !track.enabled);
|
|
setIsVideoOnHold(!isVideoOnHold);
|
|
}
|
|
}
|
|
|
|
const handleEndCall = useCallback(() => {
|
|
peer.peer.close();
|
|
|
|
if (myStream) {
|
|
myStream.getTracks().forEach(track => track.stop());
|
|
setMyStream(null);
|
|
}
|
|
|
|
setRemoteStream(null);
|
|
setCallButton(true);
|
|
setIsSendButtonVisible(true);
|
|
setIsConnecting(false);
|
|
|
|
if (remoteSocketId) {
|
|
socket.emit("call:end", { to: remoteSocketId });
|
|
}
|
|
setRemoteSocketId(null);
|
|
}, [myStream, remoteSocketId, socket]);
|
|
|
|
const handleGoBack = () => {
|
|
handleEndCall();
|
|
|
|
// Clear session data when leaving room
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.removeItem('userEmail');
|
|
localStorage.removeItem('currentRoom');
|
|
}
|
|
|
|
router.push('/');
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{!hasJoinedRoom ? (
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-indigo-900 flex items-center justify-center">
|
|
<div className="text-center text-white">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent mx-auto mb-4"></div>
|
|
<p>Verifying access...</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className='min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-indigo-900 relative overflow-hidden'>
|
|
<title>Room {slug} - VideoPeersJS</title>
|
|
|
|
{/* Background decorative elements */}
|
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
<div className="absolute top-1/4 -left-4 w-72 h-72 bg-blue-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-pulse"></div>
|
|
<div className="absolute bottom-1/4 -right-4 w-72 h-72 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-pulse delay-1000"></div>
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<motion.header
|
|
className="absolute top-0 left-0 right-0 z-20 p-6"
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6 }}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<motion.button
|
|
onClick={handleGoBack}
|
|
className="flex items-center space-x-2 px-4 py-2 bg-white/10 backdrop-blur-sm rounded-xl text-white hover:bg-white/20 transition-all duration-200"
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
<span>Back</span>
|
|
</motion.button>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Video className="h-6 w-6 text-indigo-400" />
|
|
<h1 className="text-xl font-bold text-white">
|
|
{process.env.NEXT_PUBLIC_APP_NAME || 'VideoPeersJS'}
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<div className="flex items-center space-x-2 px-4 py-2 bg-white/10 backdrop-blur-sm rounded-xl text-white">
|
|
<Users className="h-4 w-4" />
|
|
<span className="text-sm">Room {slug}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.header>
|
|
|
|
{/* Connection Status */}
|
|
<motion.div
|
|
className="absolute top-24 left-1/2 transform -translate-x-1/2 z-20"
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.2 }}
|
|
>
|
|
<div className={`px-6 py-3 rounded-full backdrop-blur-sm text-white text-center ${
|
|
remoteSocketId ? 'bg-green-500/20 border border-green-400/30' : 'bg-orange-500/20 border border-orange-400/30'
|
|
}`}>
|
|
<p className="text-sm font-medium">
|
|
{isConnecting ? (
|
|
<span className="flex items-center space-x-2">
|
|
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
|
|
<span>Connecting...</span>
|
|
</span>
|
|
) : remoteSocketId ? (
|
|
"🟢 Connected with remote user"
|
|
) : (
|
|
"🟡 Waiting for someone to join..."
|
|
)}
|
|
</p>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Video Container */}
|
|
<div className="relative h-screen flex items-center justify-center p-6 pt-32">
|
|
{/* Remote Stream (Main) */}
|
|
{remoteStream ? (
|
|
<VideoPlayer
|
|
stream={remoteStream}
|
|
name={"Remote Stream"}
|
|
isAudioMute={false}
|
|
/>
|
|
) : (
|
|
<motion.div
|
|
className="w-full max-w-4xl aspect-video bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl border-2 border-white/10 flex items-center justify-center"
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.6 }}
|
|
>
|
|
<div className="text-center text-white/60">
|
|
<Users className="h-24 w-24 mx-auto mb-4 opacity-40" />
|
|
<p className="text-xl font-medium mb-2">Waiting for remote video...</p>
|
|
<p className="text-sm">Share this room ID with someone to start a call</p>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* My Stream (Picture-in-Picture) */}
|
|
{myStream && (
|
|
<VideoPlayer
|
|
stream={myStream}
|
|
name={"My Stream"}
|
|
isAudioMute={isAudioMute}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-30">
|
|
{remoteStream && isSendButtonVisible && (
|
|
<motion.button
|
|
onClick={sendStreams}
|
|
className="mb-4 flex items-center space-x-2 px-6 py-3 bg-green-500 hover:bg-green-600 text-white rounded-xl font-semibold transition-all duration-200"
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
>
|
|
<Send className="h-5 w-5" />
|
|
<span>Send Stream</span>
|
|
</motion.button>
|
|
)}
|
|
|
|
{remoteSocketId && callButton && !remoteStream && (
|
|
<motion.button
|
|
onClick={handleCallUser}
|
|
disabled={isConnecting}
|
|
className="flex items-center space-x-2 px-8 py-4 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white rounded-2xl font-semibold text-lg shadow-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
|
whileHover={{ scale: isConnecting ? 1 : 1.05 }}
|
|
whileTap={{ scale: isConnecting ? 1 : 0.95 }}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
>
|
|
{isConnecting ? (
|
|
<>
|
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-white border-t-transparent"></div>
|
|
<span>Connecting...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Phone className="h-6 w-6" />
|
|
<span>Start Call</span>
|
|
</>
|
|
)}
|
|
</motion.button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Call Control Buttons */}
|
|
{myStream && remoteStream && !isSendButtonVisible && (
|
|
<CallHandleButtons
|
|
isAudioMute={isAudioMute}
|
|
isVideoOnHold={isVideoOnHold}
|
|
onToggleAudio={handleToggleAudio}
|
|
onToggleVideo={handleToggleVideo}
|
|
onEndCall={handleEndCall}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default RoomPage; |