From d7bbc40d28dee39738f0cb34ac749e9c56fbb094 Mon Sep 17 00:00:00 2001 From: Sinan Date: Sun, 29 Sep 2024 12:31:51 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20video=20call=20setup=20initail?= =?UTF-8?q?=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../video-section/[sectionId]/Join-page.tsx | 41 +++++++ .../video-section/[sectionId]/page.tsx | 87 ++++++++------- .../doctor/video-call/[sectionId]/page.tsx | 71 ++++++------ client/lib/hooks/video/usePatient.ts | 2 +- client/lib/socket.io/connectSocketIO.ts | 3 +- client/lib/webrtc/createPeerConnection.ts | 104 +++++++++++------- .../presentation/socket/VideoSocketManager.ts | 2 +- 7 files changed, 189 insertions(+), 121 deletions(-) create mode 100644 client/app/(patient)/video-section/[sectionId]/Join-page.tsx diff --git a/client/app/(patient)/video-section/[sectionId]/Join-page.tsx b/client/app/(patient)/video-section/[sectionId]/Join-page.tsx new file mode 100644 index 00000000..4ce52724 --- /dev/null +++ b/client/app/(patient)/video-section/[sectionId]/Join-page.tsx @@ -0,0 +1,41 @@ +import { Button } from "@/components/ui/button" +import { IVideoSection } from "@/types/entities"; +import { Video } from "lucide-react" + +interface JoinPageProps { + onJoin: () => void; + section:IVideoSection +} + +export default function JoinPage({ onJoin , section}: JoinPageProps) { + // useEffect(() => { + // if (section) { + // const checkMeetingTime = () => { + // const now = new Date() + // const meetingTime = new Date(section.startTime!) + // const timeDiff = meetingTime.getTime() - now.getTime() + // const minutesDiff = Math.floor(timeDiff / (1000 * 60)) + // setCanStartMeeting(minutesDiff <= 10 && minutesDiff >= 0) + // } + + // checkMeetingTime() + // const timer = setInterval(checkMeetingTime, 60000) + + // return () => clearInterval(timer) + // } + // }, [section]) + + + return ( +
+
+

Welcome to Your Video Call

+

Click the button below to join the room

+
+ +
+ ) +} \ No newline at end of file diff --git a/client/app/(patient)/video-section/[sectionId]/page.tsx b/client/app/(patient)/video-section/[sectionId]/page.tsx index 15707445..64b34221 100644 --- a/client/app/(patient)/video-section/[sectionId]/page.tsx +++ b/client/app/(patient)/video-section/[sectionId]/page.tsx @@ -1,59 +1,64 @@ 'use client' -import { Card } from "@/components/ui/card" -import { VideoIcon, PhoneIcon } from "lucide-react" -import VideoChat from '@/components/page-components/video/VideoChat' -import { ButtonV2 } from '@/components/button/ButtonV2' -import { useEffect, useState } from "react" -import { useParams } from "next/navigation" - -export default function VideoCallPage() { - const [isCalling, setIsCalling] = useState(false); +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import JoinPage from './Join-page'; +import VideoChat from '@/components/page-components/video/VideoChat'; +import createPeerConnection from '@/lib/webrtc/createPeerConnection'; +import { useGetSectionByIdPatient } from '@/lib/hooks/video/usePatient'; + +export default function PatientVideoCallPage() { + const { sectionId } = useParams(); + const [hasJoined, setHasJoined] = useState(false); const [localStream, setLocalStream] = useState(null); - const sectionId = useParams().sectionId as string; const [remoteStream, setRemoteStream] = useState(null); + const { data, isLoading } = useGetSectionByIdPatient(sectionId as string); + const section = data?.section; useEffect(() => { - const timer = setTimeout(() => { - setRemoteStream(new MediaStream()); - }, 2000); - - return () => clearTimeout(timer); - }, [sectionId]); - + if (hasJoined && section && localStream) { + createPeerConnection(section.roomId ?? "id", 'patient', localStream).then(connection => { + if (connection) { + setRemoteStream(connection.remoteStream); + return () => connection.peerConnection.close(); + } + }); + } + }, [hasJoined, section, localStream]); - const handleStartCall = async () => { + const handleJoin = async () => { try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) - setLocalStream(stream) - setIsCalling(true) - + const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); + setLocalStream(stream); + setHasJoined(true); } catch (error) { - console.error('Error accessing media devices:', error) + console.error('Error accessing media devices:', error); } - } + }; const handleEndCall = () => { if (localStream) { - localStream.getTracks().forEach(track => track.stop()) + localStream.getTracks().forEach(track => track.stop()); } setLocalStream(null); - setIsCalling(false); + setRemoteStream(null); + setHasJoined(false); + }; + + if (isLoading) return
Loading...
; + + if (!hasJoined) { + return ; } return ( -
- {isCalling ? ( - - ) : ( - - -

Start a Video Call

- - Start Call - -
- )} -
- ) -} \ No newline at end of file + + ); +} diff --git a/client/app/doctor/video-call/[sectionId]/page.tsx b/client/app/doctor/video-call/[sectionId]/page.tsx index df2c823e..230fd64d 100644 --- a/client/app/doctor/video-call/[sectionId]/page.tsx +++ b/client/app/doctor/video-call/[sectionId]/page.tsx @@ -1,57 +1,54 @@ 'use client' -import { useEffect, useState } from 'react' -import { useParams } from 'next/navigation' -import { useGetSectionByIdDoctor } from '@/lib/hooks/video/useDoctor' -import JoinPage from './Join-page' -import VideoChat from '@/components/page-components/video/VideoChat' -import createPeerConnection from '@/lib/webrtc/createPeerConnection' +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import JoinPage from './Join-page'; +import VideoChat from '@/components/page-components/video/VideoChat'; +import createPeerConnection from '@/lib/webrtc/createPeerConnection'; +import { useGetSectionByIdDoctor } from '@/lib/hooks/video/useDoctor'; -export default function VideoCallPage() { - const { sectionId } = useParams() - const [hasJoined, setHasJoined] = useState(false) - const [localStream, setLocalStream] = useState(null) - const [remoteStream, setRemoteStream] = useState(null) - const { data, isLoading } = useGetSectionByIdDoctor(sectionId as string) +export default function DoctorVideoCallPage() { + const { sectionId } = useParams(); + const [hasJoined, setHasJoined] = useState(false); + const [localStream, setLocalStream] = useState(null); + const [remoteStream, setRemoteStream] = useState(null); + const { data, isLoading } = useGetSectionByIdDoctor(sectionId as string); const section = data?.section; - + useEffect(() => { - if (hasJoined && section) { - const connection = createPeerConnection(section.roomId ?? "id", 'doctor'); - - if (connection) { - setRemoteStream(connection.remoteStream); - - return () => { - connection.peerConnection.close(); - }; - } + if (hasJoined && section && localStream) { + createPeerConnection(section.roomId ?? "id", 'doctor', localStream).then(connection => { + if (connection) { + setRemoteStream(connection.remoteStream); + return () => connection.peerConnection.close(); + } + }); } - }, [hasJoined, section]); + }, [hasJoined, section, localStream]); const handleJoin = async () => { try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) - setLocalStream(stream) - setHasJoined(true) + const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); + setLocalStream(stream); + setHasJoined(true); } catch (error) { - console.error('Error accessing media devices:', error) + console.error('Error accessing media devices:', error); } - } + }; const handleEndCall = () => { if (localStream) { - localStream.getTracks().forEach(track => track.stop()) + localStream.getTracks().forEach(track => track.stop()); } - setLocalStream(null) - setRemoteStream(null) - setHasJoined(false) - } + setLocalStream(null); + setRemoteStream(null); + setHasJoined(false); + }; - if (isLoading) return
Loading...
+ if (isLoading) return
Loading...
; if (!hasJoined) { - return + return ; } return ( @@ -63,5 +60,5 @@ export default function VideoCallPage() { selfAvatar={section?.doctorProfile!} remoteAvatar={section?.patientProfile!} /> - ) + ); } diff --git a/client/lib/hooks/video/usePatient.ts b/client/lib/hooks/video/usePatient.ts index 89692658..de8fd4dc 100644 --- a/client/lib/hooks/video/usePatient.ts +++ b/client/lib/hooks/video/usePatient.ts @@ -12,7 +12,7 @@ export const useGetSectionsInOneDayPatient = () => { }; export const useGetSectionByIdPatient = (sectionId: string) => { - return useQuery>({ + return useQuery<{section:IVideoSection}, AxiosError>({ queryKey: ["section-patient", sectionId], queryFn: () => getSectionByIdPatient(sectionId), }); diff --git a/client/lib/socket.io/connectSocketIO.ts b/client/lib/socket.io/connectSocketIO.ts index 97c78da6..fa71ab44 100644 --- a/client/lib/socket.io/connectSocketIO.ts +++ b/client/lib/socket.io/connectSocketIO.ts @@ -19,11 +19,10 @@ const connectSocketIO = ({ role, namespace }: Props) => { token = auth.doctorToken; } - const socket = io(`${process.env.NEXT_PUBLIC_API_URL!}/${namespace}`, { + const socket = io(`${process.env.NEXT_PUBLIC_API_URL?.split('/api')[0]}/${namespace}`, { auth: { token: token, }, - }); existingSocket = socket; diff --git a/client/lib/webrtc/createPeerConnection.ts b/client/lib/webrtc/createPeerConnection.ts index 23d733a7..0e28b327 100644 --- a/client/lib/webrtc/createPeerConnection.ts +++ b/client/lib/webrtc/createPeerConnection.ts @@ -1,45 +1,71 @@ import peerConfiguration from "@/config/webRTCStuntServer"; import connectSocketIO from "../socket.io/connectSocketIO"; -const createPeerConnection = (roomId: string, role: 'patient' | 'doctor'): { - peerConnection: RTCPeerConnection, - remoteStream: MediaStream -}|null => { - try { - const socket = connectSocketIO({ role, namespace: 'video' }); - const peerConnection = new RTCPeerConnection(peerConfiguration); - - const remoteStream = new MediaStream(); - - peerConnection.onicecandidate = (event) => { - if (event.candidate) { - socket.emit('ice-candidate', event.candidate, roomId); - } - }; - - peerConnection.ontrack = (event) => { - event.streams[0].getTracks().forEach((track) => { - remoteStream.addTrack(track); - }); - }; - - socket.on('ice-candidate', (candidate) => { - peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); - }); - - socket.on('offer', (offer) => { - peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); - peerConnection.createAnswer().then((answer) => { - peerConnection.setLocalDescription(answer); - socket.emit('answer', answer, roomId); - }); - }); - - return { peerConnection, remoteStream }; - } catch (error) { - console.error(error); - return null; +const createPeerConnection = async ( + roomId: string, + role: 'patient' | 'doctor', + localStream: MediaStream +): Promise<{ + peerConnection: RTCPeerConnection, + remoteStream: MediaStream +} | null> => { + try { + const socket = connectSocketIO({ role, namespace: 'video' }); + const peerConnection = new RTCPeerConnection(peerConfiguration); + const remoteStream = new MediaStream(); + + // Join the room + socket.emit('join-room', roomId); + + // Add local stream tracks to peer connection + localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream)); + + // Handle onicecandidate event + peerConnection.onicecandidate = (event) => { + if (event.candidate) { + socket.emit('ice-candidate', event.candidate, roomId); + } + }; + + // Handle incoming remote stream + peerConnection.ontrack = (event) => { + event.streams[0].getTracks().forEach((track) => { + remoteStream.addTrack(track); + }); + }; + + // Listen for ice candidates from server + socket.on('ice-candidate', (candidate) => { + peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); + }); + + if (role === 'doctor') { + // Doctor creates and sends offer + const offer = await peerConnection.createOffer(); + await peerConnection.setLocalDescription(offer); + socket.emit('offer', offer, roomId); } -} + + // Handle offer and send answer if patient + if (role === 'patient') { + socket.on('offer', async (offer) => { + await peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); + const answer = await peerConnection.createAnswer(); + await peerConnection.setLocalDescription(answer); + socket.emit('answer', answer, roomId); + }); + } + + // Handle answer if doctor + socket.on('answer', async (answer) => { + await peerConnection.setRemoteDescription(new RTCSessionDescription(answer)); + }); + + return { peerConnection, remoteStream }; + } catch (error) { + console.error('Error in createPeerConnection:', error); + return null; + } +}; export default createPeerConnection; diff --git a/server/src/presentation/socket/VideoSocketManager.ts b/server/src/presentation/socket/VideoSocketManager.ts index 36bc4091..161a87e1 100644 --- a/server/src/presentation/socket/VideoSocketManager.ts +++ b/server/src/presentation/socket/VideoSocketManager.ts @@ -10,8 +10,8 @@ export default class VideoSocketManager { private initializeVideoNamespace() { this.io.on("connection", (socket: Socket) => { - logger.info(`User connected to Video namespace: ${socket.id}`); + logger.info(`User connected to Video namespace: ${socket.id}`); // Join a video room socket.on("join-room", (roomId) => { socket.join(roomId);