Skip to content

Commit

Permalink
🚀 video call setup initail done
Browse files Browse the repository at this point in the history
  • Loading branch information
sinanptm committed Sep 29, 2024
1 parent 48d2105 commit d7bbc40
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 121 deletions.
41 changes: 41 additions & 0 deletions client/app/(patient)/video-section/[sectionId]/Join-page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold mb-4">Welcome to Your Video Call</h1>
<p className="text-xl text-gray-400">Click the button below to join the room</p>
</div>
<Button onClick={onJoin} size="lg" className="bg-blue-600 hover:bg-blue-700 text-white">
<Video className="mr-2 h-5 w-5" />
Join Video Call
</Button>
</div>
)
}
87 changes: 46 additions & 41 deletions client/app/(patient)/video-section/[sectionId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<MediaStream | null>(null);
const sectionId = useParams().sectionId as string;
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(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 <div className="flex items-center justify-center h-screen">Loading...</div>;

if (!hasJoined) {
return <JoinPage onJoin={handleJoin} section={section!} />;
}

return (
<div className="h-screen flex flex-col bg-gradient-to-b text-white">
{isCalling ? (
<VideoChat localStream={localStream} handleEndCall={handleEndCall} remoteStream={remoteStream} isDoctor={false} remoteAvatar="/assets/icons/circle-user" selfAvatar="/assets/icons/circle-user" />
) : (
<Card className="flex flex-col items-center justify-center flex-grow bg-transparent border-0 shadow-none">
<VideoIcon className="w-24 h-24 mb-8 text-blue-500" />
<h1 className="text-4xl font-bold mb-6 text-center">Start a Video Call</h1>
<ButtonV2 onClick={handleStartCall} size="lg" className="bg-blue-500 hover:bg-blue-600" variant={'ringHover'}>
<PhoneIcon className="mr-2 h-5 w-5" /> Start Call
</ButtonV2>
</Card>
)}
</div>
)
}
<VideoChat
localStream={localStream}
remoteStream={remoteStream}
handleEndCall={handleEndCall}
isDoctor={false}
selfAvatar={section?.patientProfile!}
remoteAvatar={section?.doctorProfile!}
/>
);
}
71 changes: 34 additions & 37 deletions client/app/doctor/video-call/[sectionId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<MediaStream | null>(null)
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null)
const { data, isLoading } = useGetSectionByIdDoctor(sectionId as string)
export default function DoctorVideoCallPage() {
const { sectionId } = useParams();
const [hasJoined, setHasJoined] = useState(false);
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(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 <div className="flex items-center justify-center h-screen">Loading...</div>
if (isLoading) return <div className="flex items-center justify-center h-screen">Loading...</div>;

if (!hasJoined) {
return <JoinPage handleStart={handleJoin} section={section!} />
return <JoinPage handleStart={handleJoin} section={section!} />;
}

return (
Expand All @@ -63,5 +60,5 @@ export default function VideoCallPage() {
selfAvatar={section?.doctorProfile!}
remoteAvatar={section?.patientProfile!}
/>
)
);
}
2 changes: 1 addition & 1 deletion client/lib/hooks/video/usePatient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const useGetSectionsInOneDayPatient = () => {
};

export const useGetSectionByIdPatient = (sectionId: string) => {
return useQuery<IVideoSection, AxiosError<ErrorResponse>>({
return useQuery<{section:IVideoSection}, AxiosError<ErrorResponse>>({
queryKey: ["section-patient", sectionId],
queryFn: () => getSectionByIdPatient(sectionId),
});
Expand Down
3 changes: 1 addition & 2 deletions client/lib/socket.io/connectSocketIO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
104 changes: 65 additions & 39 deletions client/lib/webrtc/createPeerConnection.ts
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion server/src/presentation/socket/VideoSocketManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

1 comment on commit d7bbc40

@vercel
Copy link

@vercel vercel bot commented on d7bbc40 Sep 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.