Skip to content

Commit e3da9d3

Browse files
committed
🎨 refactor: Migrate video chat from P2P to SFU architecture using MediaSoup
- Replace P2P WebRTC implementation with MediaSoup-based SFU architecture - Restructure LiveClass component for scalable video streaming: - Add ConnectionStatus for improved connection state management - Extract ControlButtons for better media control handling - Create VideoBox for unified video stream rendering - Implement MediaSoup producer/consumer pattern - Enhance WebSocket signaling for SFU communication - Add robust error handling and reconnection logic - Improve stream management and cleanup Technical Details: - Switch from direct P2P connections to MediaSoup-based routing - Implement proper stream producer/consumer lifecycle - Add TypeScript interfaces for MediaSoup integration - Update WebSocket protocol for SFU requirements Dependencies: - Add mediasoup-client - Add @types/webrtc
1 parent 1fccb21 commit e3da9d3

File tree

6 files changed

+872
-268
lines changed

6 files changed

+872
-268
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
"@tanstack/react-query": "^5.17.10",
2727
"@types/event-source-polyfill": "^1.0.5",
2828
"@types/js-cookie": "^3.0.6",
29+
"@types/webrtc": "^0.0.44",
2930
"axios": "^1.6.5",
3031
"date-fns": "^3.6.0",
3132
"event-source-polyfill": "^1.0.31",
3233
"ion-sdk-js": "^1.8.2",
3334
"js-cookie": "^3.0.5",
35+
"mediasoup-client": "^3.7.17",
3436
"moment": "^2.30.1",
3537
"next": "^14.2.7",
3638
"react": "^18",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, {memo} from 'react';
2+
3+
interface ConnectionStatusProps {
4+
state: 'connecting' | 'connected' | 'disconnected';
5+
}
6+
7+
const ConnectionStatus: React.FC<ConnectionStatusProps> = memo(({state}) => {
8+
const getStatusData = (): {color: string; text: string} => {
9+
const statusMap = {
10+
connected: {color: 'bg-green-500', text: '接続中'},
11+
connecting: {color: 'bg-yellow-500', text: '接続試行中...'},
12+
disconnected: {color: 'bg-red-500', text: '未接続'},
13+
};
14+
return statusMap[state];
15+
};
16+
17+
const {color, text} = getStatusData();
18+
19+
return (
20+
<div
21+
className="flex items-center mb-4"
22+
role="status"
23+
aria-live="polite"
24+
aria-atomic="true"
25+
>
26+
<div
27+
className={`w-3 h-3 rounded-full ${color} mr-2`}
28+
aria-hidden="true"
29+
/>
30+
<span className="text-sm text-gray-600">{text}</span>
31+
</div>
32+
);
33+
});
34+
35+
ConnectionStatus.displayName = 'ConnectionStatus';
36+
export default ConnectionStatus;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, {memo} from 'react';
2+
3+
interface MediaState {
4+
video: boolean;
5+
audio: boolean;
6+
}
7+
8+
interface ControlButtonsProps {
9+
isTeacher: boolean;
10+
isSharingScreen: boolean;
11+
mediaState: MediaState;
12+
isLoading?: boolean;
13+
disabled?: boolean;
14+
onEndClass: () => void;
15+
onToggleScreen: () => void;
16+
onToggleAudio: () => void;
17+
onToggleVideo: () => void;
18+
}
19+
20+
const ControlButtons: React.FC<ControlButtonsProps> = memo(
21+
({
22+
isTeacher,
23+
isSharingScreen,
24+
mediaState,
25+
isLoading = false,
26+
disabled = false,
27+
onEndClass,
28+
onToggleScreen,
29+
onToggleAudio,
30+
onToggleVideo,
31+
}) => {
32+
const buttonClass = (active: boolean, color: string) =>
33+
`${active ? `bg-${color}-500 hover:bg-${color}-600` : 'bg-gray-500'}
34+
text-white font-bold py-2 px-4 rounded-md transition-colors
35+
disabled:opacity-50 disabled:cursor-not-allowed`;
36+
37+
return (
38+
<div className="flex flex-col items-center gap-4">
39+
<button
40+
onClick={onEndClass}
41+
disabled={disabled || isLoading}
42+
className={buttonClass(true, 'red')}
43+
aria-label="授業を終了する"
44+
>
45+
{isLoading ? '処理中...' : '授業終了'}
46+
</button>
47+
48+
{isTeacher && (
49+
<button
50+
onClick={onToggleScreen}
51+
disabled={disabled}
52+
className={buttonClass(
53+
isSharingScreen,
54+
isSharingScreen ? 'yellow' : 'green'
55+
)}
56+
aria-label={
57+
isSharingScreen ? '画面共有を停止する' : '画面共有を開始する'
58+
}
59+
>
60+
{isSharingScreen ? '画面共有停止' : '画面共有開始'}
61+
</button>
62+
)}
63+
64+
<div className="flex justify-center gap-4">
65+
<button
66+
onClick={onToggleAudio}
67+
disabled={disabled}
68+
className={buttonClass(mediaState.audio, 'blue')}
69+
aria-label={
70+
mediaState.audio ? 'マイクをオフにする' : 'マイクをオンにする'
71+
}
72+
>
73+
{mediaState.audio ? 'マイクオン' : 'マイクオフ'}
74+
</button>
75+
<button
76+
onClick={onToggleVideo}
77+
disabled={disabled}
78+
className={buttonClass(mediaState.video, 'blue')}
79+
aria-label={
80+
mediaState.video ? 'カメラをオフにする' : 'カメラをオンにする'
81+
}
82+
>
83+
{mediaState.video ? 'カメラオン' : 'カメラオフ'}
84+
</button>
85+
</div>
86+
</div>
87+
);
88+
}
89+
);
90+
91+
ControlButtons.displayName = 'ControlButtons';
92+
export default ControlButtons;

0 commit comments

Comments
 (0)