Skip to content

Commit 618595e

Browse files
authored
Merge pull request #149 from Regulus0811/feat/live-class
ビデオチャットフロントエンドコードのアップロード
2 parents e142491 + f3de687 commit 618595e

File tree

7 files changed

+916
-15
lines changed

7 files changed

+916
-15
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"axios": "^1.6.5",
3030
"js-cookie": "^3.0.5",
3131
"moment": "^2.30.1",
32+
"ion-sdk-js": "^1.8.2",
3233
"next": "14.0.4",
3334
"react": "^18",
3435
"react-calendar": "^4.8.0",
@@ -39,6 +40,8 @@
3940
"react-time-picker": "^7.0.0",
4041
"recoil": "^0.7.7",
4142
"recoil-persist": "^5.1.0",
43+
"simple-peer": "^9.11.1",
44+
"socket.io-client": "^4.7.5",
4245
"swiper": "^11.1.0",
4346
"ts-node": "^10.9.2"
4447
},

src/api/_class/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import getClassInfo from './getClassInfo';
22
import getClasses from './getClasses';
33
import getClassesRole from './getClassesRole';
44
import getFavoriteClasses from './getFavoriteClasses';
5-
import patchToggleFavoriteClass from './patchToggleFavoriteClass';
5+
// import patchClassRole from './patchClassRole';
6+
// import patchToggleFavoriteClass from './patchToggleFavoriteClass';
67
import postCreateClass from './postCreateClass';
78

89
const classAPI = {
910
getClassInfo,
1011
getClasses,
1112
getClassesRole,
1213
getFavoriteClasses,
13-
patchToggleFavoriteClass,
14+
// patchClassRole,
15+
// patchToggleFavoriteClass,
1416
postCreateClass,
1517
};
1618

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
/* eslint-disable react-hooks/exhaustive-deps */
2+
// import React, {useEffect, useRef} from 'react';
3+
4+
// interface LiveClassProps {
5+
// classId: number;
6+
// userId: number;
7+
// }
8+
9+
// const LiveClass: React.FC<LiveClassProps> = ({classId, userId}) => {
10+
// const videoRef = useRef<HTMLVideoElement | null>(null);
11+
// const pcRef = useRef<RTCPeerConnection | null>(null);
12+
// const wsRef = useRef<WebSocket | null>(null);
13+
14+
// useEffect(() => {
15+
// const pc = new RTCPeerConnection({
16+
// iceServers: [{urls: 'stun:stun.l.google.com:19302'}],
17+
// });
18+
// pcRef.current = pc;
19+
20+
// const ws = new WebSocket(
21+
// `ws://localhost:8080/?classId=${classId}&userId=${userId}`
22+
// );
23+
// wsRef.current = ws;
24+
25+
// ws.onopen = async () => {
26+
// console.log('WebSocket connected');
27+
// if (pcRef.current) {
28+
// const offer = await pcRef.current.createOffer();
29+
// await pcRef.current.setLocalDescription(offer);
30+
// console.log('Created offer:', offer);
31+
// if (wsRef.current && pcRef.current.localDescription) {
32+
// wsRef.current.send(
33+
// JSON.stringify({
34+
// event: 'offer',
35+
// data: pcRef.current.localDescription,
36+
// })
37+
// );
38+
// console.log('Sent offer:', pcRef.current.localDescription);
39+
// }
40+
// }
41+
// };
42+
43+
// ws.onmessage = async event => {
44+
// const {event: evt, data} = JSON.parse(event.data);
45+
// console.log('Message received:', evt, data);
46+
// if (evt === 'answer') {
47+
// await pc.setRemoteDescription(new RTCSessionDescription(data));
48+
// } else if (evt === 'candidate') {
49+
// await pc.addIceCandidate(new RTCIceCandidate(data));
50+
// }
51+
// };
52+
53+
// ws.onclose = () => {
54+
// console.log('WebSocket closed');
55+
// };
56+
57+
// pc.onicecandidate = event => {
58+
// if (event.candidate && wsRef.current) {
59+
// wsRef.current.send(
60+
// JSON.stringify({event: 'candidate', data: event.candidate.toJSON()})
61+
// );
62+
// }
63+
// };
64+
65+
// pc.ontrack = event => {
66+
// if (videoRef.current) {
67+
// videoRef.current.srcObject = event.streams[0];
68+
// }
69+
// };
70+
71+
// return () => {
72+
// pcRef.current?.close();
73+
// wsRef.current?.close();
74+
// };
75+
// }, [classId, userId]);
76+
77+
// const startScreenShare = async () => {
78+
// try {
79+
// const mediaStream = await navigator.mediaDevices.getDisplayMedia({
80+
// video: true,
81+
// audio: true,
82+
// });
83+
// mediaStream
84+
// .getTracks()
85+
// .forEach(track => pcRef.current?.addTrack(track, mediaStream));
86+
// if (videoRef.current) {
87+
// videoRef.current.srcObject = mediaStream;
88+
// }
89+
90+
// if (wsRef.current && pcRef.current) {
91+
// const offer = await pcRef.current.createOffer();
92+
// await pcRef.current.setLocalDescription(offer);
93+
// wsRef.current.send(
94+
// JSON.stringify({event: 'offer', data: pcRef.current.localDescription})
95+
// );
96+
// }
97+
// console.log('Screen sharing started');
98+
// } catch (error) {
99+
// console.error('Screen sharing failed', error);
100+
// }
101+
// };
102+
103+
// return (
104+
// <div>
105+
// <video
106+
// ref={videoRef}
107+
// autoPlay
108+
// playsInline
109+
// controls
110+
// style={{width: '100%'}}
111+
// />
112+
// <button onClick={startScreenShare}>Start Screen Sharing</button>
113+
// </div>
114+
// );
115+
// };
116+
117+
// export default LiveClass;
118+
119+
/* eslint-disable @typescript-eslint/no-explicit-any */
120+
import React, {useEffect, useRef, useState} from 'react';
121+
122+
interface LiveClassProps {
123+
classId: number;
124+
userId: number;
125+
}
126+
127+
const LiveClass: React.FC<LiveClassProps> = ({classId, userId}) => {
128+
const [classStarted, setClassStarted] = useState(false);
129+
const localVideoRef = useRef<HTMLVideoElement | null>(null);
130+
const peerConnections = useRef<{[key: string]: RTCPeerConnection}>({});
131+
const remoteVideoRefs = useRef<{[key: string]: HTMLVideoElement | null}>({});
132+
const wsRef = useRef<WebSocket | null>(null);
133+
const iceCandidatesRef = useRef<{[key: string]: any[]}>({});
134+
135+
const startWebSocket = () => {
136+
const ws = new WebSocket(
137+
`ws://localhost:8080/?classId=${classId}&userId=${userId}`
138+
);
139+
wsRef.current = ws;
140+
141+
ws.onopen = async () => {
142+
console.log('WebSocket connected');
143+
};
144+
145+
ws.onmessage = async event => {
146+
const {event: evt, data, from} = JSON.parse(event.data);
147+
console.log('Message received:', evt, data, from);
148+
149+
if (evt === 'offer') {
150+
const pc = createPeerConnection(from);
151+
await pc.setRemoteDescription(new RTCSessionDescription(data));
152+
const answer = await pc.createAnswer();
153+
await pc.setLocalDescription(answer);
154+
ws.send(JSON.stringify({event: 'answer', data: answer, to: from}));
155+
} else if (evt === 'answer') {
156+
const pc = peerConnections.current[from];
157+
if (pc) {
158+
await pc.setRemoteDescription(new RTCSessionDescription(data));
159+
}
160+
} else if (evt === 'candidate') {
161+
const pc = peerConnections.current[from];
162+
if (pc) {
163+
await pc.addIceCandidate(new RTCIceCandidate(data));
164+
} else {
165+
if (!iceCandidatesRef.current[from]) {
166+
iceCandidatesRef.current[from] = [];
167+
}
168+
iceCandidatesRef.current[from].push(data);
169+
}
170+
}
171+
};
172+
173+
ws.onclose = () => {
174+
console.log('WebSocket closed');
175+
};
176+
};
177+
178+
const createPeerConnection = (peerId: string) => {
179+
const pc = new RTCPeerConnection({
180+
iceServers: [{urls: 'stun:stun.l.google.com:19302'}],
181+
});
182+
peerConnections.current[peerId] = pc;
183+
184+
pc.onicecandidate = event => {
185+
if (event.candidate) {
186+
wsRef.current?.send(
187+
JSON.stringify({
188+
event: 'candidate',
189+
data: event.candidate,
190+
to: peerId,
191+
})
192+
);
193+
}
194+
};
195+
196+
pc.ontrack = event => {
197+
if (!remoteVideoRefs.current[peerId]) {
198+
remoteVideoRefs.current[peerId] = document.createElement('video');
199+
remoteVideoRefs.current[peerId]!.autoplay = true;
200+
remoteVideoRefs.current[peerId]!.playsInline = true;
201+
document
202+
.getElementById('remoteVideos')
203+
?.appendChild(remoteVideoRefs.current[peerId]!);
204+
}
205+
remoteVideoRefs.current[peerId]!.srcObject = event.streams[0];
206+
};
207+
208+
if (iceCandidatesRef.current[peerId]) {
209+
iceCandidatesRef.current[peerId].forEach(candidate => {
210+
pc.addIceCandidate(new RTCIceCandidate(candidate));
211+
});
212+
iceCandidatesRef.current[peerId] = [];
213+
}
214+
215+
return pc;
216+
};
217+
218+
useEffect(() => {
219+
if (classStarted) {
220+
startWebSocket();
221+
222+
return () => {
223+
// eslint-disable-next-line react-hooks/exhaustive-deps
224+
Object.values(peerConnections.current).forEach(pc => pc.close());
225+
if (wsRef.current) wsRef.current.close();
226+
// eslint-disable-next-line react-hooks/exhaustive-deps
227+
if (localVideoRef.current) localVideoRef.current.srcObject = null;
228+
Object.values(remoteVideoRefs.current).forEach(video => {
229+
if (video) video.srcObject = null;
230+
});
231+
remoteVideoRefs.current = {};
232+
};
233+
}
234+
}, [classStarted]);
235+
236+
const handleStartClass = async () => {
237+
setClassStarted(true);
238+
const mediaStream = await navigator.mediaDevices.getUserMedia({
239+
video: true,
240+
audio: true,
241+
});
242+
if (localVideoRef.current) {
243+
localVideoRef.current.srcObject = mediaStream;
244+
}
245+
wsRef.current?.send(JSON.stringify({event: 'join', data: null}));
246+
};
247+
248+
const handleEndClass = () => {
249+
setClassStarted(false);
250+
};
251+
252+
return (
253+
<div>
254+
{!classStarted ? (
255+
<button onClick={handleStartClass}>Start Class</button>
256+
) : (
257+
<button onClick={handleEndClass}>End Class</button>
258+
)}
259+
{classStarted && (
260+
<div>
261+
<video ref={localVideoRef} autoPlay playsInline muted />
262+
<div id="remoteVideos"></div>
263+
</div>
264+
)}
265+
</div>
266+
);
267+
};
268+
269+
export default LiveClass;
Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1-
const manageSubContainer = () => {
1+
import React from 'react';
2+
import LiveClass from './LiveClass';
3+
import {useParams} from 'next/navigation';
4+
import {useRecoilValue} from 'recoil';
5+
import {User} from '@/src/interfaces/user';
6+
import userState from '@/src/recoil/atoms/userState';
7+
8+
const ManageSubContainer: React.FC = () => {
9+
const {cId} = useParams<{cId: string}>();
10+
const classId = parseInt(cId, 10);
11+
const user = useRecoilValue(userState) as User;
12+
13+
if (isNaN(classId) || user.id === 0) {
14+
return <div>loading...</div>;
15+
}
16+
217
return (
318
<div>
4-
<h1>ManageSubContainer</h1>
19+
<LiveClass classId={classId} userId={user.id} />
520
</div>
621
);
722
};
823

9-
export default manageSubContainer;
24+
export default ManageSubContainer;

0 commit comments

Comments
 (0)