AeThex-Connect/astro-site/src/react-app/utils/webrtc.js
MrPiglr de54903c15
new file: astro-site/src/components/auth/SupabaseLogin.jsx
new file:   astro-site/src/components/auth/SupabaseLogin.jsx
2026-02-03 09:09:36 +00:00

532 lines
14 KiB
JavaScript

/**
* WebRTC Manager
* Handles all WebRTC peer connection logic for voice and video calls
*/
class WebRTCManager {
constructor(socket) {
this.socket = socket;
this.peerConnections = new Map(); // Map of userId -> RTCPeerConnection
this.localStream = null;
this.screenStream = null;
this.remoteStreams = new Map(); // Map of userId -> MediaStream
this.currentCallId = null;
this.isInitiator = false;
// WebRTC configuration with STUN/TURN servers
this.configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
// Media constraints
this.audioConstraints = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
};
this.videoConstraints = {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
};
// Event handlers
this.onRemoteStream = null;
this.onRemoteStreamRemoved = null;
this.onConnectionStateChange = null;
this.onIceConnectionStateChange = null;
this.setupSocketListeners();
}
/**
* Setup Socket.io listeners for WebRTC signaling
*/
setupSocketListeners() {
this.socket.on('call:offer', async (data) => {
await this.handleOffer(data);
});
this.socket.on('call:answer', async (data) => {
await this.handleAnswer(data);
});
this.socket.on('call:ice-candidate', async (data) => {
await this.handleIceCandidate(data);
});
this.socket.on('call:ended', () => {
this.cleanup();
});
this.socket.on('call:participant-joined', async (data) => {
console.log('Participant joined:', data);
// For group calls, establish connection with new participant
if (this.isInitiator) {
await this.initiateCallToUser(data.userId);
}
});
this.socket.on('call:participant-left', (data) => {
console.log('Participant left:', data);
this.removePeerConnection(data.userId);
});
this.socket.on('call:media-state-changed', (data) => {
console.log('Media state changed:', data);
// Update UI to reflect remote user's media state
if (this.onMediaStateChanged) {
this.onMediaStateChanged(data);
}
});
}
/**
* Set TURN server credentials
*/
async setTurnCredentials(turnCredentials) {
if (turnCredentials && turnCredentials.urls) {
const turnServer = {
urls: turnCredentials.urls,
username: turnCredentials.username,
credential: turnCredentials.credential
};
this.configuration.iceServers.push(turnServer);
console.log('TURN server configured');
}
}
/**
* Initialize local media stream (audio and/or video)
*/
async initializeLocalStream(audioEnabled = true, videoEnabled = true) {
try {
const constraints = {
audio: audioEnabled ? this.audioConstraints : false,
video: videoEnabled ? this.videoConstraints : false
};
this.localStream = await navigator.mediaDevices.getUserMedia(constraints);
console.log('Local stream initialized:', {
audio: audioEnabled,
video: videoEnabled,
tracks: this.localStream.getTracks().length
});
return this.localStream;
} catch (error) {
console.error('Error accessing media devices:', error);
throw new Error(`Failed to access camera/microphone: ${error.message}`);
}
}
/**
* Create a peer connection for a user
*/
createPeerConnection(userId) {
if (this.peerConnections.has(userId)) {
return this.peerConnections.get(userId);
}
const peerConnection = new RTCPeerConnection(this.configuration);
// Add local stream tracks to peer connection
if (this.localStream) {
this.localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, this.localStream);
});
}
// Handle incoming remote stream
peerConnection.ontrack = (event) => {
console.log('Received remote track from', userId, event.track.kind);
const [remoteStream] = event.streams;
this.remoteStreams.set(userId, remoteStream);
if (this.onRemoteStream) {
this.onRemoteStream(userId, remoteStream);
}
};
// Handle ICE candidates
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
console.log('Sending ICE candidate to', userId);
this.socket.emit('call:ice-candidate', {
callId: this.currentCallId,
targetUserId: userId,
candidate: event.candidate
});
}
};
// Handle connection state changes
peerConnection.onconnectionstatechange = () => {
console.log(`Connection state with ${userId}:`, peerConnection.connectionState);
if (this.onConnectionStateChange) {
this.onConnectionStateChange(userId, peerConnection.connectionState);
}
// Cleanup if connection fails or closes
if (peerConnection.connectionState === 'failed' ||
peerConnection.connectionState === 'closed') {
this.removePeerConnection(userId);
}
};
// Handle ICE connection state changes
peerConnection.oniceconnectionstatechange = () => {
console.log(`ICE connection state with ${userId}:`, peerConnection.iceConnectionState);
if (this.onIceConnectionStateChange) {
this.onIceConnectionStateChange(userId, peerConnection.iceConnectionState);
}
};
this.peerConnections.set(userId, peerConnection);
return peerConnection;
}
/**
* Initiate a call to a user (create offer)
*/
async initiateCallToUser(userId) {
try {
const peerConnection = this.createPeerConnection(userId);
// Create offer
const offer = await peerConnection.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
await peerConnection.setLocalDescription(offer);
// Send offer through signaling server
this.socket.emit('call:offer', {
callId: this.currentCallId,
targetUserId: userId,
offer: offer
});
console.log('Call offer sent to', userId);
} catch (error) {
console.error('Error initiating call:', error);
throw error;
}
}
/**
* Handle incoming call offer
*/
async handleOffer(data) {
const { callId, fromUserId, offer } = data;
try {
console.log('Received call offer from', fromUserId);
this.currentCallId = callId;
const peerConnection = this.createPeerConnection(fromUserId);
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
// Create answer
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
// Send answer back
this.socket.emit('call:answer', {
callId: callId,
targetUserId: fromUserId,
answer: answer
});
console.log('Call answer sent to', fromUserId);
} catch (error) {
console.error('Error handling offer:', error);
throw error;
}
}
/**
* Handle incoming call answer
*/
async handleAnswer(data) {
const { fromUserId, answer } = data;
try {
console.log('Received call answer from', fromUserId);
const peerConnection = this.peerConnections.get(fromUserId);
if (!peerConnection) {
throw new Error(`No peer connection found for user ${fromUserId}`);
}
await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
console.log('Remote description set for', fromUserId);
} catch (error) {
console.error('Error handling answer:', error);
throw error;
}
}
/**
* Handle incoming ICE candidate
*/
async handleIceCandidate(data) {
const { fromUserId, candidate } = data;
try {
const peerConnection = this.peerConnections.get(fromUserId);
if (!peerConnection) {
console.warn(`No peer connection found for user ${fromUserId}`);
return;
}
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
console.log('ICE candidate added for', fromUserId);
} catch (error) {
console.error('Error adding ICE candidate:', error);
}
}
/**
* Remove peer connection for a user
*/
removePeerConnection(userId) {
const peerConnection = this.peerConnections.get(userId);
if (peerConnection) {
peerConnection.close();
this.peerConnections.delete(userId);
}
const remoteStream = this.remoteStreams.get(userId);
if (remoteStream) {
remoteStream.getTracks().forEach(track => track.stop());
this.remoteStreams.delete(userId);
if (this.onRemoteStreamRemoved) {
this.onRemoteStreamRemoved(userId);
}
}
console.log('Peer connection removed for', userId);
}
/**
* Toggle audio track enabled/disabled
*/
toggleAudio(enabled) {
if (this.localStream) {
const audioTrack = this.localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = enabled;
console.log('Audio', enabled ? 'enabled' : 'disabled');
return true;
}
}
return false;
}
/**
* Toggle video track enabled/disabled
*/
toggleVideo(enabled) {
if (this.localStream) {
const videoTrack = this.localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = enabled;
console.log('Video', enabled ? 'enabled' : 'disabled');
return true;
}
}
return false;
}
/**
* Start screen sharing
*/
async startScreenShare() {
try {
this.screenStream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'always'
},
audio: false
});
const screenTrack = this.screenStream.getVideoTracks()[0];
// Replace video track in all peer connections
this.peerConnections.forEach((peerConnection) => {
const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
if (sender) {
sender.replaceTrack(screenTrack);
}
});
// Handle screen share stop
screenTrack.onended = () => {
this.stopScreenShare();
};
console.log('Screen sharing started');
return this.screenStream;
} catch (error) {
console.error('Error starting screen share:', error);
throw error;
}
}
/**
* Stop screen sharing and restore camera
*/
stopScreenShare() {
if (this.screenStream) {
this.screenStream.getTracks().forEach(track => track.stop());
this.screenStream = null;
// Restore camera track
if (this.localStream) {
const videoTrack = this.localStream.getVideoTracks()[0];
if (videoTrack) {
this.peerConnections.forEach((peerConnection) => {
const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
if (sender) {
sender.replaceTrack(videoTrack);
}
});
}
}
console.log('Screen sharing stopped');
}
}
/**
* Get connection statistics
*/
async getConnectionStats(userId) {
const peerConnection = this.peerConnections.get(userId);
if (!peerConnection) {
return null;
}
const stats = await peerConnection.getStats();
const result = {
audio: {},
video: {},
connection: {}
};
stats.forEach(report => {
if (report.type === 'inbound-rtp') {
if (report.kind === 'audio') {
result.audio.bytesReceived = report.bytesReceived;
result.audio.packetsLost = report.packetsLost;
result.audio.jitter = report.jitter;
} else if (report.kind === 'video') {
result.video.bytesReceived = report.bytesReceived;
result.video.packetsLost = report.packetsLost;
result.video.framesDecoded = report.framesDecoded;
result.video.frameWidth = report.frameWidth;
result.video.frameHeight = report.frameHeight;
}
} else if (report.type === 'candidate-pair' && report.state === 'succeeded') {
result.connection.roundTripTime = report.currentRoundTripTime;
result.connection.availableOutgoingBitrate = report.availableOutgoingBitrate;
}
});
return result;
}
/**
* Cleanup all connections and streams
*/
cleanup() {
console.log('Cleaning up WebRTC resources');
// Stop screen share if active
this.stopScreenShare();
// Close all peer connections
this.peerConnections.forEach((peerConnection, userId) => {
this.removePeerConnection(userId);
});
// Stop local stream
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop());
this.localStream = null;
}
// Clear remote streams
this.remoteStreams.forEach((stream) => {
stream.getTracks().forEach(track => track.stop());
});
this.remoteStreams.clear();
this.currentCallId = null;
this.isInitiator = false;
}
/**
* Get local stream
*/
getLocalStream() {
return this.localStream;
}
/**
* Get remote stream for a user
*/
getRemoteStream(userId) {
return this.remoteStreams.get(userId);
}
/**
* Get all remote streams
*/
getAllRemoteStreams() {
return Array.from(this.remoteStreams.entries());
}
/**
* Check if audio is enabled
*/
isAudioEnabled() {
if (this.localStream) {
const audioTrack = this.localStream.getAudioTracks()[0];
return audioTrack ? audioTrack.enabled : false;
}
return false;
}
/**
* Check if video is enabled
*/
isVideoEnabled() {
if (this.localStream) {
const videoTrack = this.localStream.getVideoTracks()[0];
return videoTrack ? videoTrack.enabled : false;
}
return false;
}
/**
* Check if screen sharing is active
*/
isScreenSharing() {
return this.screenStream !== null;
}
}
export default WebRTCManager;