diff --git a/package.json b/package.json index fcfdd98..799152e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "axios": "^1.6.5", "js-cookie": "^3.0.5", "moment": "^2.30.1", + "ion-sdk-js": "^1.8.2", "next": "14.0.4", "react": "^18", "react-calendar": "^4.8.0", @@ -39,6 +40,8 @@ "react-time-picker": "^7.0.0", "recoil": "^0.7.7", "recoil-persist": "^5.1.0", + "simple-peer": "^9.11.1", + "socket.io-client": "^4.7.5", "swiper": "^11.1.0", "ts-node": "^10.9.2" }, diff --git a/src/api/_class/index.ts b/src/api/_class/index.ts index 97c1eb2..43074ae 100644 --- a/src/api/_class/index.ts +++ b/src/api/_class/index.ts @@ -2,7 +2,8 @@ import getClassInfo from './getClassInfo'; import getClasses from './getClasses'; import getClassesRole from './getClassesRole'; import getFavoriteClasses from './getFavoriteClasses'; -import patchToggleFavoriteClass from './patchToggleFavoriteClass'; +// import patchClassRole from './patchClassRole'; +// import patchToggleFavoriteClass from './patchToggleFavoriteClass'; import postCreateClass from './postCreateClass'; const classAPI = { @@ -10,7 +11,8 @@ const classAPI = { getClasses, getClassesRole, getFavoriteClasses, - patchToggleFavoriteClass, + // patchClassRole, + // patchToggleFavoriteClass, postCreateClass, }; diff --git a/src/app/classes/[cId]/[mId]/components/manageSubComponents/LiveClass.tsx b/src/app/classes/[cId]/[mId]/components/manageSubComponents/LiveClass.tsx new file mode 100644 index 0000000..2556f98 --- /dev/null +++ b/src/app/classes/[cId]/[mId]/components/manageSubComponents/LiveClass.tsx @@ -0,0 +1,269 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +// import React, {useEffect, useRef} from 'react'; + +// interface LiveClassProps { +// classId: number; +// userId: number; +// } + +// const LiveClass: React.FC = ({classId, userId}) => { +// const videoRef = useRef(null); +// const pcRef = useRef(null); +// const wsRef = useRef(null); + +// useEffect(() => { +// const pc = new RTCPeerConnection({ +// iceServers: [{urls: 'stun:stun.l.google.com:19302'}], +// }); +// pcRef.current = pc; + +// const ws = new WebSocket( +// `ws://localhost:8080/?classId=${classId}&userId=${userId}` +// ); +// wsRef.current = ws; + +// ws.onopen = async () => { +// console.log('WebSocket connected'); +// if (pcRef.current) { +// const offer = await pcRef.current.createOffer(); +// await pcRef.current.setLocalDescription(offer); +// console.log('Created offer:', offer); +// if (wsRef.current && pcRef.current.localDescription) { +// wsRef.current.send( +// JSON.stringify({ +// event: 'offer', +// data: pcRef.current.localDescription, +// }) +// ); +// console.log('Sent offer:', pcRef.current.localDescription); +// } +// } +// }; + +// ws.onmessage = async event => { +// const {event: evt, data} = JSON.parse(event.data); +// console.log('Message received:', evt, data); +// if (evt === 'answer') { +// await pc.setRemoteDescription(new RTCSessionDescription(data)); +// } else if (evt === 'candidate') { +// await pc.addIceCandidate(new RTCIceCandidate(data)); +// } +// }; + +// ws.onclose = () => { +// console.log('WebSocket closed'); +// }; + +// pc.onicecandidate = event => { +// if (event.candidate && wsRef.current) { +// wsRef.current.send( +// JSON.stringify({event: 'candidate', data: event.candidate.toJSON()}) +// ); +// } +// }; + +// pc.ontrack = event => { +// if (videoRef.current) { +// videoRef.current.srcObject = event.streams[0]; +// } +// }; + +// return () => { +// pcRef.current?.close(); +// wsRef.current?.close(); +// }; +// }, [classId, userId]); + +// const startScreenShare = async () => { +// try { +// const mediaStream = await navigator.mediaDevices.getDisplayMedia({ +// video: true, +// audio: true, +// }); +// mediaStream +// .getTracks() +// .forEach(track => pcRef.current?.addTrack(track, mediaStream)); +// if (videoRef.current) { +// videoRef.current.srcObject = mediaStream; +// } + +// if (wsRef.current && pcRef.current) { +// const offer = await pcRef.current.createOffer(); +// await pcRef.current.setLocalDescription(offer); +// wsRef.current.send( +// JSON.stringify({event: 'offer', data: pcRef.current.localDescription}) +// ); +// } +// console.log('Screen sharing started'); +// } catch (error) { +// console.error('Screen sharing failed', error); +// } +// }; + +// return ( +//
+//
+// ); +// }; + +// export default LiveClass; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React, {useEffect, useRef, useState} from 'react'; + +interface LiveClassProps { + classId: number; + userId: number; +} + +const LiveClass: React.FC = ({classId, userId}) => { + const [classStarted, setClassStarted] = useState(false); + const localVideoRef = useRef(null); + const peerConnections = useRef<{[key: string]: RTCPeerConnection}>({}); + const remoteVideoRefs = useRef<{[key: string]: HTMLVideoElement | null}>({}); + const wsRef = useRef(null); + const iceCandidatesRef = useRef<{[key: string]: any[]}>({}); + + const startWebSocket = () => { + const ws = new WebSocket( + `ws://localhost:8080/?classId=${classId}&userId=${userId}` + ); + wsRef.current = ws; + + ws.onopen = async () => { + console.log('WebSocket connected'); + }; + + ws.onmessage = async event => { + const {event: evt, data, from} = JSON.parse(event.data); + console.log('Message received:', evt, data, from); + + if (evt === 'offer') { + const pc = createPeerConnection(from); + await pc.setRemoteDescription(new RTCSessionDescription(data)); + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + ws.send(JSON.stringify({event: 'answer', data: answer, to: from})); + } else if (evt === 'answer') { + const pc = peerConnections.current[from]; + if (pc) { + await pc.setRemoteDescription(new RTCSessionDescription(data)); + } + } else if (evt === 'candidate') { + const pc = peerConnections.current[from]; + if (pc) { + await pc.addIceCandidate(new RTCIceCandidate(data)); + } else { + if (!iceCandidatesRef.current[from]) { + iceCandidatesRef.current[from] = []; + } + iceCandidatesRef.current[from].push(data); + } + } + }; + + ws.onclose = () => { + console.log('WebSocket closed'); + }; + }; + + const createPeerConnection = (peerId: string) => { + const pc = new RTCPeerConnection({ + iceServers: [{urls: 'stun:stun.l.google.com:19302'}], + }); + peerConnections.current[peerId] = pc; + + pc.onicecandidate = event => { + if (event.candidate) { + wsRef.current?.send( + JSON.stringify({ + event: 'candidate', + data: event.candidate, + to: peerId, + }) + ); + } + }; + + pc.ontrack = event => { + if (!remoteVideoRefs.current[peerId]) { + remoteVideoRefs.current[peerId] = document.createElement('video'); + remoteVideoRefs.current[peerId]!.autoplay = true; + remoteVideoRefs.current[peerId]!.playsInline = true; + document + .getElementById('remoteVideos') + ?.appendChild(remoteVideoRefs.current[peerId]!); + } + remoteVideoRefs.current[peerId]!.srcObject = event.streams[0]; + }; + + if (iceCandidatesRef.current[peerId]) { + iceCandidatesRef.current[peerId].forEach(candidate => { + pc.addIceCandidate(new RTCIceCandidate(candidate)); + }); + iceCandidatesRef.current[peerId] = []; + } + + return pc; + }; + + useEffect(() => { + if (classStarted) { + startWebSocket(); + + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + Object.values(peerConnections.current).forEach(pc => pc.close()); + if (wsRef.current) wsRef.current.close(); + // eslint-disable-next-line react-hooks/exhaustive-deps + if (localVideoRef.current) localVideoRef.current.srcObject = null; + Object.values(remoteVideoRefs.current).forEach(video => { + if (video) video.srcObject = null; + }); + remoteVideoRefs.current = {}; + }; + } + }, [classStarted]); + + const handleStartClass = async () => { + setClassStarted(true); + const mediaStream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true, + }); + if (localVideoRef.current) { + localVideoRef.current.srcObject = mediaStream; + } + wsRef.current?.send(JSON.stringify({event: 'join', data: null})); + }; + + const handleEndClass = () => { + setClassStarted(false); + }; + + return ( +
+ {!classStarted ? ( + + ) : ( + + )} + {classStarted && ( +
+
+ )} +
+ ); +}; + +export default LiveClass; diff --git a/src/app/classes/[cId]/[mId]/components/manageSubComponents/ManageSubContainer.tsx b/src/app/classes/[cId]/[mId]/components/manageSubComponents/ManageSubContainer.tsx index fc03e55..ef6635d 100644 --- a/src/app/classes/[cId]/[mId]/components/manageSubComponents/ManageSubContainer.tsx +++ b/src/app/classes/[cId]/[mId]/components/manageSubComponents/ManageSubContainer.tsx @@ -1,9 +1,24 @@ -const manageSubContainer = () => { +import React from 'react'; +import LiveClass from './LiveClass'; +import {useParams} from 'next/navigation'; +import {useRecoilValue} from 'recoil'; +import {User} from '@/src/interfaces/user'; +import userState from '@/src/recoil/atoms/userState'; + +const ManageSubContainer: React.FC = () => { + const {cId} = useParams<{cId: string}>(); + const classId = parseInt(cId, 10); + const user = useRecoilValue(userState) as User; + + if (isNaN(classId) || user.id === 0) { + return
loading...
; + } + return (
-

ManageSubContainer

+
); }; -export default manageSubContainer; +export default ManageSubContainer; diff --git a/src/app/classes/[cId]/[mId]/components/subComponents/LiveClassViewer.tsx b/src/app/classes/[cId]/[mId]/components/subComponents/LiveClassViewer.tsx new file mode 100644 index 0000000..cb33b8c --- /dev/null +++ b/src/app/classes/[cId]/[mId]/components/subComponents/LiveClassViewer.tsx @@ -0,0 +1,241 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +// import React, {useEffect, useRef} from 'react'; + +// interface LiveClassViewerProps { +// classId: number; +// userId: number; +// } + +// const LiveClassViewer: React.FC = ({classId, userId}) => { +// const videoRef = useRef(null); +// const pcRef = useRef(null); +// const wsRef = useRef(null); + +// useEffect(() => { +// const pc = new RTCPeerConnection({ +// iceServers: [{urls: 'stun:stun.l.google.com:19302'}], +// }); +// pcRef.current = pc; + +// const ws = new WebSocket( +// `ws://localhost:8080/?classId=${classId}&userId=${userId}` +// ); +// wsRef.current = ws; + +// ws.onopen = () => { +// console.log('WebSocket opened'); +// }; + +// ws.onmessage = async event => { +// const {event: evt, data} = JSON.parse(event.data); +// console.log('Message received:', evt, data); +// if (evt === 'offer' && pcRef.current) { +// try { +// console.log('Received offer:', data); +// await pcRef.current.setRemoteDescription( +// new RTCSessionDescription(data) +// ); +// console.log('Set remote description'); +// const answer = await pcRef.current.createAnswer(); +// console.log('Created answer:', answer); +// await pcRef.current.setLocalDescription(answer); +// console.log('Set local description with answer'); +// if (wsRef.current) { +// wsRef.current.send(JSON.stringify({event: 'answer', data: answer})); +// console.log('Sent answer:', answer); +// } +// } catch (error) { +// console.error('Failed to handle offer:', error); +// } +// } else if (evt === 'candidate' && pcRef.current) { +// try { +// console.log('Received candidate:', data); +// await pcRef.current.addIceCandidate(new RTCIceCandidate(data)); +// console.log('Added ICE candidate'); +// } catch (error) { +// console.error('Failed to add ICE candidate:', error); +// } +// } +// }; + +// ws.onclose = () => { +// console.log('WebSocket closed'); +// }; + +// pc.onicecandidate = event => { +// if (event.candidate && wsRef.current) { +// wsRef.current.send( +// JSON.stringify({event: 'candidate', data: event.candidate.toJSON()}) +// ); +// } +// }; + +// pc.ontrack = event => { +// if (videoRef.current) { +// videoRef.current.srcObject = event.streams[0]; +// } +// }; + +// return () => { +// pc.close(); +// ws.close(); +// }; +// }, [classId, userId]); + +// return ( +//