mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 10:20:15 +01:00
✨ feat: stream back audio to user (test)
This commit is contained in:
parent
964d47cfa3
commit
7aed891838
6 changed files with 151 additions and 49 deletions
|
|
@ -82,7 +82,6 @@
|
|||
"mongoose": "^8.9.5",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nanoid": "^3.3.7",
|
||||
"node-pre-gyp": "^0.17.0",
|
||||
"nodemailer": "^6.9.15",
|
||||
"ollama": "^0.5.0",
|
||||
"openai": "^4.47.1",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { Server } = require('socket.io');
|
||||
const { RTCPeerConnection, RTCIceCandidate } = require('wrtc');
|
||||
const { RTCPeerConnection, RTCIceCandidate, MediaStream } = require('wrtc');
|
||||
|
||||
class WebRTCConnection {
|
||||
constructor(socket, config) {
|
||||
|
|
@ -14,38 +14,31 @@ class WebRTCConnection {
|
|||
|
||||
async handleOffer(offer) {
|
||||
try {
|
||||
// Create new peer connection if needed
|
||||
if (!this.peerConnection) {
|
||||
this.peerConnection = new RTCPeerConnection(this.config.rtcConfig);
|
||||
this.setupPeerConnectionListeners();
|
||||
}
|
||||
|
||||
// Set the remote description (client's offer)
|
||||
await this.peerConnection.setRemoteDescription(offer);
|
||||
|
||||
// Set up audio transceiver for two-way audio
|
||||
// Create MediaStream instance properly
|
||||
const mediaStream = new MediaStream();
|
||||
|
||||
this.audioTransceiver = this.peerConnection.addTransceiver('audio', {
|
||||
direction: 'sendrecv',
|
||||
streams: [mediaStream],
|
||||
});
|
||||
|
||||
// Create and set local description (answer)
|
||||
const answer = await this.peerConnection.createAnswer();
|
||||
await this.peerConnection.setLocalDescription(answer);
|
||||
|
||||
// Send answer to client
|
||||
this.socket.emit('webrtc-answer', answer);
|
||||
|
||||
// Process any pending ICE candidates
|
||||
while (this.pendingCandidates.length) {
|
||||
const candidate = this.pendingCandidates.shift();
|
||||
await this.addIceCandidate(candidate);
|
||||
}
|
||||
|
||||
this.state = 'connecting';
|
||||
} catch (error) {
|
||||
this.log(`Error handling offer: ${error}`, 'error');
|
||||
this.socket.emit('error', { message: 'Failed to process offer' });
|
||||
this.cleanup();
|
||||
// Don't throw, handle gracefully
|
||||
this.socket.emit('webrtc-error', {
|
||||
message: error.message,
|
||||
code: 'OFFER_ERROR',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ export const Call: React.FC = () => {
|
|||
useEffect(() => {
|
||||
if (remoteAudioRef.current && remoteStream) {
|
||||
remoteAudioRef.current.srcObject = remoteStream;
|
||||
|
||||
remoteAudioRef.current.play().catch((err) => console.error('Error playing audio:', err));
|
||||
}
|
||||
}, [remoteStream]);
|
||||
|
||||
|
|
@ -98,6 +100,36 @@ export const Call: React.FC = () => {
|
|||
const isActive = callState === CallState.ACTIVE;
|
||||
const isError = callState === CallState.ERROR;
|
||||
|
||||
// TESTS
|
||||
|
||||
useEffect(() => {
|
||||
if (remoteAudioRef.current && remoteStream) {
|
||||
console.log('Setting up remote audio:', {
|
||||
tracks: remoteStream.getTracks().length,
|
||||
active: remoteStream.active,
|
||||
});
|
||||
|
||||
remoteAudioRef.current.srcObject = remoteStream;
|
||||
remoteAudioRef.current.muted = false;
|
||||
remoteAudioRef.current.volume = 1.0;
|
||||
|
||||
const playPromise = remoteAudioRef.current.play();
|
||||
if (playPromise) {
|
||||
playPromise.catch((err) => {
|
||||
console.error('Error playing audio:', err);
|
||||
// Retry play on user interaction
|
||||
document.addEventListener(
|
||||
'click',
|
||||
() => {
|
||||
remoteAudioRef.current?.play();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [remoteStream]);
|
||||
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={setOpen}>
|
||||
<OGDialogContent className="w-[28rem] p-8">
|
||||
|
|
@ -200,7 +232,11 @@ export const Call: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Hidden Audio Element */}
|
||||
<audio ref={remoteAudioRef} autoPlay>
|
||||
<audio
|
||||
ref={remoteAudioRef}
|
||||
autoPlay
|
||||
playsInline
|
||||
>
|
||||
<track kind="captions" />
|
||||
</audio>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,46 @@ const useCall = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const handleRemoteStream = (stream: MediaStream | null) => {
|
||||
console.log('[WebRTC] Remote stream received:', {
|
||||
stream: stream,
|
||||
active: stream?.active,
|
||||
tracks: stream?.getTracks().map((t) => ({
|
||||
kind: t.kind,
|
||||
enabled: t.enabled,
|
||||
muted: t.muted,
|
||||
})),
|
||||
});
|
||||
|
||||
if (!stream) {
|
||||
console.error('[WebRTC] Received null remote stream');
|
||||
updateStatus({
|
||||
error: {
|
||||
code: 'NO_REMOTE_STREAM',
|
||||
message: 'No remote stream received',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
if (!audioTracks.length) {
|
||||
console.error('[WebRTC] No audio tracks in remote stream');
|
||||
updateStatus({
|
||||
error: {
|
||||
code: 'NO_AUDIO_TRACKS',
|
||||
message: 'Remote stream contains no audio',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus({
|
||||
remoteStream: stream,
|
||||
callState: CallState.ACTIVE,
|
||||
});
|
||||
};
|
||||
|
||||
const handleConnectionStateChange = useCallback(
|
||||
(state: ConnectionState) => {
|
||||
switch (state) {
|
||||
|
|
@ -123,6 +163,7 @@ const useCall = () => {
|
|||
|
||||
const startCall = useCallback(async () => {
|
||||
if (!isConnected) {
|
||||
console.log('Cannot start call - not connected to server');
|
||||
updateStatus({
|
||||
callState: CallState.ERROR,
|
||||
error: {
|
||||
|
|
@ -134,7 +175,10 @@ const useCall = () => {
|
|||
}
|
||||
|
||||
try {
|
||||
console.log('Starting new call...');
|
||||
|
||||
if (webrtcServiceRef.current) {
|
||||
console.log('Cleaning up existing WebRTC connection');
|
||||
webrtcServiceRef.current.close();
|
||||
}
|
||||
|
||||
|
|
@ -149,13 +193,15 @@ const useCall = () => {
|
|||
debug: true,
|
||||
});
|
||||
|
||||
webrtcServiceRef.current.on('connectionStateChange', handleConnectionStateChange);
|
||||
|
||||
webrtcServiceRef.current.on('remoteStream', (stream: MediaStream) => {
|
||||
updateStatus({ remoteStream: stream });
|
||||
webrtcServiceRef.current.on('connectionStateChange', (state: ConnectionState) => {
|
||||
console.log('WebRTC connection state changed:', state);
|
||||
handleConnectionStateChange(state);
|
||||
});
|
||||
|
||||
webrtcServiceRef.current.on('remoteStream', handleRemoteStream);
|
||||
|
||||
webrtcServiceRef.current.on('error', (error: string) => {
|
||||
console.error('WebRTC error:', error);
|
||||
updateStatus({
|
||||
callState: CallState.ERROR,
|
||||
isConnecting: false,
|
||||
|
|
@ -166,9 +212,13 @@ const useCall = () => {
|
|||
});
|
||||
});
|
||||
|
||||
console.log('Initializing WebRTC connection...');
|
||||
await webrtcServiceRef.current.initialize();
|
||||
console.log('WebRTC initialization complete');
|
||||
|
||||
startConnectionMonitoring();
|
||||
} catch (error) {
|
||||
console.error('Failed to start call:', error);
|
||||
updateStatus({
|
||||
callState: CallState.ERROR,
|
||||
isConnecting: false,
|
||||
|
|
@ -203,9 +253,11 @@ const useCall = () => {
|
|||
useEffect(() => {
|
||||
const cleanupFns = [
|
||||
addEventListener(WebSocketEvents.WEBRTC_ANSWER, (answer: RTCSessionDescriptionInit) => {
|
||||
console.log('Received WebRTC answer:', answer);
|
||||
webrtcServiceRef.current?.handleAnswer(answer);
|
||||
}),
|
||||
addEventListener(WebSocketEvents.ICE_CANDIDATE, (candidate: RTCIceCandidateInit) => {
|
||||
console.log('Received ICE candidate:', candidate);
|
||||
webrtcServiceRef.current?.addIceCandidate(candidate);
|
||||
}),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class WebRTCService extends EventEmitter {
|
|||
},
|
||||
],
|
||||
maxReconnectAttempts: 3,
|
||||
connectionTimeout: 15000,
|
||||
connectionTimeout: 30000,
|
||||
debug: false,
|
||||
};
|
||||
|
||||
|
|
@ -124,30 +124,49 @@ export class WebRTCService extends EventEmitter {
|
|||
}
|
||||
|
||||
this.peerConnection.ontrack = ({ track, streams }) => {
|
||||
this.log('Received remote track:', track.kind);
|
||||
this.remoteStream = streams[0];
|
||||
this.emit('remoteStream', this.remoteStream);
|
||||
};
|
||||
this.log('Track received:', {
|
||||
kind: track.kind,
|
||||
enabled: track.enabled,
|
||||
readyState: track.readyState,
|
||||
});
|
||||
|
||||
this.peerConnection.onicecandidate = ({ candidate }) => {
|
||||
if (candidate) {
|
||||
this.sendSignalingMessage({
|
||||
type: 'icecandidate',
|
||||
payload: candidate.toJSON(),
|
||||
if (track.kind === 'audio') {
|
||||
// Create remote stream if needed
|
||||
if (!this.remoteStream) {
|
||||
this.remoteStream = new MediaStream();
|
||||
}
|
||||
|
||||
// Add incoming track to remote stream
|
||||
this.remoteStream.addTrack(track);
|
||||
|
||||
// Echo back the track
|
||||
if (this.peerConnection) {
|
||||
this.peerConnection.addTrack(track, this.remoteStream);
|
||||
}
|
||||
|
||||
this.log('Audio track added to remote stream', {
|
||||
tracks: this.remoteStream.getTracks().length,
|
||||
active: this.remoteStream.active,
|
||||
});
|
||||
|
||||
this.emit('remoteStream', this.remoteStream);
|
||||
}
|
||||
};
|
||||
|
||||
this.peerConnection.onconnectionstatechange = () => {
|
||||
const state = this.peerConnection?.connectionState;
|
||||
if (!this.peerConnection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = this.peerConnection.connectionState;
|
||||
this.log('Connection state changed:', state);
|
||||
|
||||
switch (state) {
|
||||
case 'connected':
|
||||
this.clearConnectionTimeout(); // Clear timeout when connected
|
||||
this.setConnectionState(ConnectionState.CONNECTED);
|
||||
this.clearConnectionTimeout();
|
||||
this.reconnectAttempts = 0;
|
||||
break;
|
||||
case 'disconnected':
|
||||
case 'failed':
|
||||
if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
|
||||
this.attemptReconnection();
|
||||
|
|
@ -155,19 +174,11 @@ export class WebRTCService extends EventEmitter {
|
|||
this.handleError(new Error('Connection failed after max reconnection attempts'));
|
||||
}
|
||||
break;
|
||||
case 'disconnected':
|
||||
this.setConnectionState(ConnectionState.RECONNECTING);
|
||||
this.attemptReconnection();
|
||||
break;
|
||||
case 'closed':
|
||||
this.setConnectionState(ConnectionState.CLOSED);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.peerConnection.oniceconnectionstatechange = () => {
|
||||
this.log('ICE connection state:', this.peerConnection?.iceConnectionState);
|
||||
};
|
||||
}
|
||||
|
||||
private async createAndSendOffer() {
|
||||
|
|
@ -221,7 +232,11 @@ export class WebRTCService extends EventEmitter {
|
|||
private startConnectionTimeout() {
|
||||
this.clearConnectionTimeout();
|
||||
this.connectionTimeoutId = setTimeout(() => {
|
||||
if (this.connectionState !== ConnectionState.CONNECTED) {
|
||||
// Only timeout if we're not in a connected or connecting state
|
||||
if (
|
||||
this.connectionState !== ConnectionState.CONNECTED &&
|
||||
this.connectionState !== ConnectionState.CONNECTING
|
||||
) {
|
||||
this.handleError(new Error('Connection timeout'));
|
||||
}
|
||||
}, this.config.connectionTimeout);
|
||||
|
|
@ -260,9 +275,17 @@ export class WebRTCService extends EventEmitter {
|
|||
private handleError(error: Error | unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
this.log('Error:', errorMessage);
|
||||
this.setConnectionState(ConnectionState.FAILED);
|
||||
this.emit('error', errorMessage);
|
||||
this.close();
|
||||
|
||||
// Don't set failed state if we're already connected
|
||||
if (this.connectionState !== ConnectionState.CONNECTED) {
|
||||
this.setConnectionState(ConnectionState.FAILED);
|
||||
this.emit('error', errorMessage);
|
||||
}
|
||||
|
||||
// Only close if we're not connected
|
||||
if (this.connectionState !== ConnectionState.CONNECTED) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
public close() {
|
||||
|
|
|
|||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -98,7 +98,6 @@
|
|||
"mongoose": "^8.9.5",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nanoid": "^3.3.7",
|
||||
"node-pre-gyp": "^0.17.0",
|
||||
"nodemailer": "^6.9.15",
|
||||
"ollama": "^0.5.0",
|
||||
"openai": "^4.47.1",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue