mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-25 12:48:53 +01:00
178 lines
4.4 KiB
JavaScript
178 lines
4.4 KiB
JavaScript
const { RTCPeerConnection, RTCIceCandidate, MediaStream } = require('wrtc');
|
|
const { logger } = require('~/config');
|
|
|
|
class WebRTCConnection {
|
|
constructor(socket, config) {
|
|
this.socket = socket;
|
|
this.config = config;
|
|
this.peerConnection = null;
|
|
this.audioTransceiver = null;
|
|
this.pendingCandidates = [];
|
|
this.state = 'idle';
|
|
}
|
|
|
|
async handleOffer(offer) {
|
|
try {
|
|
if (!this.peerConnection) {
|
|
this.peerConnection = new RTCPeerConnection(this.config.rtcConfig);
|
|
this.setupPeerConnectionListeners();
|
|
}
|
|
|
|
await this.peerConnection.setRemoteDescription(offer);
|
|
|
|
const mediaStream = new MediaStream();
|
|
|
|
this.audioTransceiver = this.peerConnection.addTransceiver('audio', {
|
|
direction: 'sendrecv',
|
|
streams: [mediaStream],
|
|
});
|
|
|
|
const answer = await this.peerConnection.createAnswer();
|
|
await this.peerConnection.setLocalDescription(answer);
|
|
this.socket.emit('webrtc-answer', answer);
|
|
} catch (error) {
|
|
logger.error(`Error handling offer: ${error}`);
|
|
this.socket.emit('webrtc-error', {
|
|
message: error.message,
|
|
code: 'OFFER_ERROR',
|
|
});
|
|
}
|
|
}
|
|
|
|
setupPeerConnectionListeners() {
|
|
if (!this.peerConnection) {
|
|
return;
|
|
}
|
|
|
|
this.peerConnection.ontrack = ({ track }) => {
|
|
logger.info(`Received ${track.kind} track from client`);
|
|
|
|
if (track.kind === 'audio') {
|
|
this.handleIncomingAudio(track);
|
|
}
|
|
|
|
track.onended = () => {
|
|
logger.info(`${track.kind} track ended`);
|
|
};
|
|
};
|
|
|
|
this.peerConnection.onicecandidate = ({ candidate }) => {
|
|
if (candidate) {
|
|
this.socket.emit('icecandidate', candidate);
|
|
}
|
|
};
|
|
|
|
this.peerConnection.onconnectionstatechange = () => {
|
|
if (!this.peerConnection) {
|
|
return;
|
|
}
|
|
|
|
const state = this.peerConnection.connectionState;
|
|
logger.info(`Connection state changed to ${state}`);
|
|
|
|
if (state === 'failed' || state === 'closed' || state === 'disconnected') {
|
|
this.cleanup();
|
|
}
|
|
};
|
|
}
|
|
|
|
handleIncomingAudio(track) {
|
|
if (this.peerConnection) {
|
|
const stream = new MediaStream([track]);
|
|
this.peerConnection.addTrack(track, stream);
|
|
}
|
|
}
|
|
|
|
async addIceCandidate(candidate) {
|
|
try {
|
|
if (this.peerConnection?.remoteDescription) {
|
|
if (candidate && candidate.candidate) {
|
|
await this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
|
|
} else {
|
|
logger.warn('Invalid ICE candidate');
|
|
}
|
|
} else {
|
|
this.pendingCandidates.push(candidate);
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Error adding ICE candidate: ${error}`);
|
|
}
|
|
}
|
|
|
|
cleanup() {
|
|
if (this.peerConnection) {
|
|
try {
|
|
this.peerConnection.close();
|
|
} catch (error) {
|
|
logger.error(`Error closing peer connection: ${error}`);
|
|
}
|
|
this.peerConnection = null;
|
|
}
|
|
|
|
this.audioTransceiver = null;
|
|
this.pendingCandidates = [];
|
|
this.state = 'idle';
|
|
}
|
|
}
|
|
|
|
class AudioHandler {
|
|
constructor() {
|
|
this.connections = new Map();
|
|
this.defaultRTCConfig = {
|
|
iceServers: [
|
|
{
|
|
urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'],
|
|
},
|
|
],
|
|
iceCandidatePoolSize: 10,
|
|
bundlePolicy: 'max-bundle',
|
|
rtcpMuxPolicy: 'require',
|
|
};
|
|
}
|
|
|
|
registerSocketHandlers(socket) {
|
|
const rtcConfig = {
|
|
rtcConfig: this.defaultRTCConfig,
|
|
};
|
|
|
|
const rtcConnection = new WebRTCConnection(socket, rtcConfig);
|
|
this.connections.set(socket.id, rtcConnection);
|
|
|
|
socket.on('webrtc-offer', (offer) => {
|
|
logger.debug(`Received WebRTC offer from ${socket.id}`);
|
|
rtcConnection.handleOffer(offer);
|
|
});
|
|
|
|
socket.on('icecandidate', (candidate) => {
|
|
rtcConnection.addIceCandidate(candidate);
|
|
});
|
|
|
|
socket.on('vad-status', (speaking) => {
|
|
logger.debug(`VAD status from ${socket.id}: ${JSON.stringify(speaking)}`);
|
|
});
|
|
|
|
socket.on('disconnect', () => {
|
|
rtcConnection.cleanup();
|
|
this.connections.delete(socket.id);
|
|
});
|
|
|
|
return rtcConnection;
|
|
}
|
|
|
|
cleanup(socketId) {
|
|
const connection = this.connections.get(socketId);
|
|
if (connection) {
|
|
connection.cleanup();
|
|
this.connections.delete(socketId);
|
|
}
|
|
}
|
|
|
|
cleanupAll() {
|
|
for (const connection of this.connections.values()) {
|
|
connection.cleanup();
|
|
}
|
|
this.connections.clear();
|
|
}
|
|
}
|
|
|
|
module.exports = { AudioHandler, WebRTCConnection };
|