129 lines
3.2 KiB
JavaScript
129 lines
3.2 KiB
JavaScript
/**
|
||
* VoiceVideoCall Component
|
||
* Displays voice/video call interface with participant videos and controls
|
||
*/
|
||
|
||
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
LiveKitRoom,
|
||
VideoConference,
|
||
GridLayout,
|
||
ParticipantTile,
|
||
useRemoteParticipant,
|
||
useLocalParticipant,
|
||
} from 'livekit-react';
|
||
import './VoiceVideoCall.css';
|
||
|
||
export default function VoiceVideoCall({
|
||
roomName,
|
||
userName,
|
||
token,
|
||
serverUrl,
|
||
onLeave,
|
||
}) {
|
||
const [callState, setCallState] = useState('connecting'); // connecting, connected, ended
|
||
|
||
if (!token || !serverUrl) {
|
||
return (
|
||
<div className="voice-video-error">
|
||
<p>Missing required configuration for video call</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="voice-video-call-container">
|
||
<LiveKitRoom
|
||
video={{ resolution: { width: 640, height: 480 } }}
|
||
audio={true}
|
||
token={token}
|
||
serverUrl={serverUrl}
|
||
onConnected={() => setCallState('connected')}
|
||
onDisconnected={() => {
|
||
setCallState('ended');
|
||
onLeave?.();
|
||
}}
|
||
options={{
|
||
adaptiveStream: true,
|
||
dynacast: true,
|
||
}}
|
||
>
|
||
{callState === 'connecting' && (
|
||
<div className="call-connecting">
|
||
<div className="spinner"></div>
|
||
<p>Connecting to {roomName}...</p>
|
||
</div>
|
||
)}
|
||
|
||
{callState === 'connected' && (
|
||
<>
|
||
<VideoConference />
|
||
<CallControls onLeave={onLeave} />
|
||
</>
|
||
)}
|
||
</LiveKitRoom>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* CallControls Component
|
||
* Displays microphone, camera, screen share, and leave buttons
|
||
*/
|
||
function CallControls({ onLeave }) {
|
||
const { localParticipant } = useLocalParticipant();
|
||
const [isMuted, setIsMuted] = useState(false);
|
||
const [isCameraOff, setIsCameraOff] = useState(false);
|
||
const [isScreenSharing, setIsScreenSharing] = useState(false);
|
||
|
||
const handleToggleMicrophone = async () => {
|
||
await localParticipant?.setMicrophoneEnabled(isMuted);
|
||
setIsMuted(!isMuted);
|
||
};
|
||
|
||
const handleToggleCamera = async () => {
|
||
await localParticipant?.setCameraEnabled(isCameraOff);
|
||
setIsCameraOff(!isCameraOff);
|
||
};
|
||
|
||
const handleToggleScreenShare = async () => {
|
||
await localParticipant?.setScreenShareEnabled(!isScreenSharing);
|
||
setIsScreenSharing(!isScreenSharing);
|
||
};
|
||
|
||
return (
|
||
<div className="call-controls">
|
||
<button
|
||
className={`control-btn ${isMuted ? 'disabled' : ''}`}
|
||
onClick={handleToggleMicrophone}
|
||
title={isMuted ? 'Unmute' : 'Mute'}
|
||
>
|
||
{isMuted ? '🔇' : '🎤'}
|
||
</button>
|
||
|
||
<button
|
||
className={`control-btn ${isCameraOff ? 'disabled' : ''}`}
|
||
onClick={handleToggleCamera}
|
||
title={isCameraOff ? 'Turn on camera' : 'Turn off camera'}
|
||
>
|
||
{isCameraOff ? '📹' : '📷'}
|
||
</button>
|
||
|
||
<button
|
||
className={`control-btn ${isScreenSharing ? 'active' : ''}`}
|
||
onClick={handleToggleScreenShare}
|
||
title={isScreenSharing ? 'Stop sharing' : 'Share screen'}
|
||
>
|
||
🖥️
|
||
</button>
|
||
|
||
<button
|
||
className="control-btn leave-btn"
|
||
onClick={onLeave}
|
||
title="Leave call"
|
||
>
|
||
☎️
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|