Files
gradio-webrtc/demo/nextjs_voice_chat/frontend/fastrtc-demo/lib/webrtc-client.ts
Rohan Richard 6905810f37 Adding nextjs + 11labs + openai streaming demo (#139)
* adding nextjs + 11labs + openai streaming demo

* removing package-lock
2025-03-07 14:24:23 -05:00

189 lines
6.6 KiB
TypeScript

interface WebRTCClientOptions {
onConnected?: () => void;
onDisconnected?: () => void;
onMessage?: (message: any) => void;
onAudioStream?: (stream: MediaStream) => void;
onAudioLevel?: (level: number) => void;
}
export class WebRTCClient {
private peerConnection: RTCPeerConnection | null = null;
private mediaStream: MediaStream | null = null;
private dataChannel: RTCDataChannel | null = null;
private options: WebRTCClientOptions;
private audioContext: AudioContext | null = null;
private analyser: AnalyserNode | null = null;
private dataArray: Uint8Array | null = null;
private animationFrameId: number | null = null;
constructor(options: WebRTCClientOptions = {}) {
this.options = options;
}
async connect() {
try {
this.peerConnection = new RTCPeerConnection();
// Get user media
try {
this.mediaStream = await navigator.mediaDevices.getUserMedia({
audio: true
});
} catch (mediaError: any) {
console.error('Media error:', mediaError);
if (mediaError.name === 'NotAllowedError') {
throw new Error('Microphone access denied. Please allow microphone access and try again.');
} else if (mediaError.name === 'NotFoundError') {
throw new Error('No microphone detected. Please connect a microphone and try again.');
} else {
throw mediaError;
}
}
this.setupAudioAnalysis();
this.mediaStream.getTracks().forEach(track => {
if (this.peerConnection) {
this.peerConnection.addTrack(track, this.mediaStream!);
}
});
this.peerConnection.addEventListener('track', (event) => {
if (this.options.onAudioStream) {
this.options.onAudioStream(event.streams[0]);
}
});
this.dataChannel = this.peerConnection.createDataChannel('text');
this.dataChannel.addEventListener('message', (event) => {
try {
const message = JSON.parse(event.data);
console.log('Received message:', message);
if (this.options.onMessage) {
this.options.onMessage(message);
}
} catch (error) {
console.error('Error parsing message:', error);
}
});
// Create and send offer
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
// Use same-origin request to avoid CORS preflight
const response = await fetch('http://localhost:8000/webrtc/offer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
mode: 'cors', // Explicitly set CORS mode
credentials: 'same-origin',
body: JSON.stringify({
sdp: offer.sdp,
type: offer.type,
webrtc_id: Math.random().toString(36).substring(7)
})
});
const serverResponse = await response.json();
await this.peerConnection.setRemoteDescription(serverResponse);
if (this.options.onConnected) {
this.options.onConnected();
}
} catch (error) {
console.error('Error connecting:', error);
this.disconnect();
throw error;
}
}
private setupAudioAnalysis() {
if (!this.mediaStream) return;
try {
this.audioContext = new AudioContext();
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 256;
const source = this.audioContext.createMediaStreamSource(this.mediaStream);
source.connect(this.analyser);
const bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(bufferLength);
this.startAnalysis();
} catch (error) {
console.error('Error setting up audio analysis:', error);
}
}
private startAnalysis() {
if (!this.analyser || !this.dataArray || !this.options.onAudioLevel) return;
// Add throttling to prevent too many updates
let lastUpdateTime = 0;
const throttleInterval = 100; // Only update every 100ms
const analyze = () => {
this.analyser!.getByteFrequencyData(this.dataArray!);
const currentTime = Date.now();
// Only update if enough time has passed since last update
if (currentTime - lastUpdateTime > throttleInterval) {
// Calculate average volume level (0-1)
let sum = 0;
for (let i = 0; i < this.dataArray!.length; i++) {
sum += this.dataArray![i];
}
const average = sum / this.dataArray!.length / 255;
this.options.onAudioLevel!(average);
lastUpdateTime = currentTime;
}
this.animationFrameId = requestAnimationFrame(analyze);
};
this.animationFrameId = requestAnimationFrame(analyze);
}
private stopAnalysis() {
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
this.analyser = null;
this.dataArray = null;
}
disconnect() {
this.stopAnalysis();
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop());
this.mediaStream = null;
}
if (this.peerConnection) {
this.peerConnection.close();
this.peerConnection = null;
}
this.dataChannel = null;
if (this.options.onDisconnected) {
this.options.onDisconnected();
}
}
}