Skip to content

Commit

Permalink
🎨 refactor: Migrate video chat from P2P to SFU architecture using Med…
Browse files Browse the repository at this point in the history
…iaSoup

- 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
  • Loading branch information
yuminn-k committed Nov 1, 2024
1 parent 1fccb21 commit e3da9d3
Show file tree
Hide file tree
Showing 6 changed files with 872 additions and 268 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
"@tanstack/react-query": "^5.17.10",
"@types/event-source-polyfill": "^1.0.5",
"@types/js-cookie": "^3.0.6",
"@types/webrtc": "^0.0.44",
"axios": "^1.6.5",
"date-fns": "^3.6.0",
"event-source-polyfill": "^1.0.31",
"ion-sdk-js": "^1.8.2",
"js-cookie": "^3.0.5",
"mediasoup-client": "^3.7.17",
"moment": "^2.30.1",
"next": "^14.2.7",
"react": "^18",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, {memo} from 'react';

interface ConnectionStatusProps {
state: 'connecting' | 'connected' | 'disconnected';
}

const ConnectionStatus: React.FC<ConnectionStatusProps> = memo(({state}) => {
const getStatusData = (): {color: string; text: string} => {
const statusMap = {
connected: {color: 'bg-green-500', text: 'ζŽ₯碚中'},
connecting: {color: 'bg-yellow-500', text: 'ζŽ₯碚試葌中...'},
disconnected: {color: 'bg-red-500', text: 'ζœͺζŽ₯碚'},
};
return statusMap[state];
};

const {color, text} = getStatusData();

return (
<div
className="flex items-center mb-4"
role="status"
aria-live="polite"
aria-atomic="true"
>
<div
className={`w-3 h-3 rounded-full ${color} mr-2`}
aria-hidden="true"
/>
<span className="text-sm text-gray-600">{text}</span>
</div>
);
});

ConnectionStatus.displayName = 'ConnectionStatus';
export default ConnectionStatus;
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, {memo} from 'react';

interface MediaState {
video: boolean;
audio: boolean;
}

interface ControlButtonsProps {
isTeacher: boolean;
isSharingScreen: boolean;
mediaState: MediaState;
isLoading?: boolean;
disabled?: boolean;
onEndClass: () => void;
onToggleScreen: () => void;
onToggleAudio: () => void;
onToggleVideo: () => void;
}

const ControlButtons: React.FC<ControlButtonsProps> = memo(
({
isTeacher,
isSharingScreen,
mediaState,
isLoading = false,
disabled = false,
onEndClass,
onToggleScreen,
onToggleAudio,
onToggleVideo,
}) => {
const buttonClass = (active: boolean, color: string) =>
`${active ? `bg-${color}-500 hover:bg-${color}-600` : 'bg-gray-500'}
text-white font-bold py-2 px-4 rounded-md transition-colors
disabled:opacity-50 disabled:cursor-not-allowed`;

return (
<div className="flex flex-col items-center gap-4">
<button
onClick={onEndClass}
disabled={disabled || isLoading}
className={buttonClass(true, 'red')}
aria-label="授ζ₯­γ‚’硂了する"
>
{isLoading ? '処理中...' : '授ζ₯­η΅‚δΊ†'}
</button>

{isTeacher && (
<button
onClick={onToggleScreen}
disabled={disabled}
className={buttonClass(
isSharingScreen,
isSharingScreen ? 'yellow' : 'green'
)}
aria-label={
isSharingScreen ? 'η”»ι’ε…±ζœ‰γ‚’εœζ­’γ™γ‚‹' : 'η”»ι’ε…±ζœ‰γ‚’ι–‹ε§‹γ™γ‚‹'
}
>
{isSharingScreen ? 'η”»ι’ε…±ζœ‰εœζ­’' : 'η”»ι’ε…±ζœ‰ι–‹ε§‹'}
</button>
)}

<div className="flex justify-center gap-4">
<button
onClick={onToggleAudio}
disabled={disabled}
className={buttonClass(mediaState.audio, 'blue')}
aria-label={
mediaState.audio ? 'γƒžγ‚€γ‚―γ‚’γ‚ͺフにする' : 'γƒžγ‚€γ‚―γ‚’γ‚ͺンにする'
}
>
{mediaState.audio ? 'γƒžγ‚€γ‚―γ‚ͺン' : 'γƒžγ‚€γ‚―γ‚ͺフ'}
</button>
<button
onClick={onToggleVideo}
disabled={disabled}
className={buttonClass(mediaState.video, 'blue')}
aria-label={
mediaState.video ? 'カパラをγ‚ͺフにする' : 'カパラをγ‚ͺンにする'
}
>
{mediaState.video ? 'カパラγ‚ͺン' : 'カパラγ‚ͺフ'}
</button>
</div>
</div>
);
}
);

ControlButtons.displayName = 'ControlButtons';
export default ControlButtons;
Loading

0 comments on commit e3da9d3

Please sign in to comment.