Merge pull request #1581 from pipecat-ai/voice_agent_ice_servers
Configuring the voice-agent example to wait for all the ice candidates.
This commit is contained in:
@@ -46,6 +46,46 @@ http://localhost:7860
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## WebRTC ICE Servers Configuration
|
||||||
|
|
||||||
|
When implementing WebRTC in your project, **STUN** (Session Traversal Utilities for NAT) and **TURN** (Traversal Using Relays around NAT)
|
||||||
|
servers are usually needed in cases where users are behind routers or firewalls.
|
||||||
|
|
||||||
|
In local networks (e.g., testing within the same home or office network), you usually don’t need to configure STUN or TURN servers.
|
||||||
|
In such cases, WebRTC can often directly establish peer-to-peer connections without needing to traverse NAT or firewalls.
|
||||||
|
|
||||||
|
### What are STUN and TURN Servers?
|
||||||
|
|
||||||
|
- **STUN Server**: Helps clients discover their public IP address and port when they're behind a NAT (Network Address Translation) device (like a router).
|
||||||
|
This allows WebRTC to attempt direct peer-to-peer communication by providing the public-facing IP and port.
|
||||||
|
|
||||||
|
- **TURN Server**: Used as a fallback when direct peer-to-peer communication isn't possible due to strict NATs or firewalls blocking connections.
|
||||||
|
The TURN server relays media traffic between peers.
|
||||||
|
|
||||||
|
### Why are ICE Servers Important?
|
||||||
|
|
||||||
|
**ICE (Interactive Connectivity Establishment)** is a framework used by WebRTC to handle network traversal and NAT issues.
|
||||||
|
The `iceServers` configuration provides a list of **STUN** and **TURN** servers that WebRTC uses to find the best way to connect two peers.
|
||||||
|
|
||||||
|
### Example Configuration for ICE Servers
|
||||||
|
|
||||||
|
Here’s how you can configure a basic `iceServers` object in WebRTC for testing purposes, using Google's public STUN server:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const config = {
|
||||||
|
iceServers: [
|
||||||
|
{
|
||||||
|
urls: ["stun:stun.l.google.com:19302"], // Google's public STUN server
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
> For testing purposes, you can either use public **STUN** servers (like Google's) or set up your own **TURN** server.
|
||||||
|
If you're running your own TURN server, make sure to include your server URL, username, and credential in the configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 💡 Notes
|
### 💡 Notes
|
||||||
- Ensure all dependencies are installed before running the server.
|
- Ensure all dependencies are installed before running the server.
|
||||||
- Check the `.env` file for missing configurations.
|
- Check the `.env` file for missing configurations.
|
||||||
|
|||||||
@@ -24,27 +24,47 @@
|
|||||||
let connected = false
|
let connected = false
|
||||||
let peerConnection = null
|
let peerConnection = null
|
||||||
|
|
||||||
/*const waitForIceGatheringComplete = async (pc) => {
|
const waitForIceGatheringComplete = async (pc, timeoutMs = 2000) => {
|
||||||
if (pc.iceGatheringState === 'complete') return;
|
if (pc.iceGatheringState === 'complete') return;
|
||||||
|
console.log("Waiting for ICE gathering to complete. Current state:", pc.iceGatheringState);
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
let timeoutId;
|
||||||
const checkState = () => {
|
const checkState = () => {
|
||||||
|
console.log("icegatheringstatechange:", pc.iceGatheringState);
|
||||||
if (pc.iceGatheringState === 'complete') {
|
if (pc.iceGatheringState === 'complete') {
|
||||||
pc.removeEventListener('icegatheringstatechange', checkState);
|
cleanup();
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const onTimeout = () => {
|
||||||
|
console.warn(`ICE gathering timed out after ${timeoutMs} ms.`);
|
||||||
|
cleanup();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const cleanup = () => {
|
||||||
|
pc.removeEventListener('icegatheringstatechange', checkState);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
pc.addEventListener('icegatheringstatechange', checkState);
|
pc.addEventListener('icegatheringstatechange', checkState);
|
||||||
|
timeoutId = setTimeout(onTimeout, timeoutMs);
|
||||||
|
// Checking the state again to avoid any eventual race condition
|
||||||
|
checkState();
|
||||||
});
|
});
|
||||||
}*/
|
};
|
||||||
|
|
||||||
|
|
||||||
const createSmallWebRTCConnection = async (audioTrack) => {
|
const createSmallWebRTCConnection = async (audioTrack) => {
|
||||||
const pc = new RTCPeerConnection()
|
const config = {
|
||||||
|
iceServers: [],
|
||||||
|
};
|
||||||
|
const pc = new RTCPeerConnection(config)
|
||||||
|
addPeerConnectionEventListeners(pc)
|
||||||
pc.ontrack = e => audioEl.srcObject = e.streams[0]
|
pc.ontrack = e => audioEl.srcObject = e.streams[0]
|
||||||
// SmallWebRTCTransport expects to receive both transceivers
|
// SmallWebRTCTransport expects to receive both transceivers
|
||||||
pc.addTransceiver(audioTrack, { direction: 'sendrecv' })
|
pc.addTransceiver(audioTrack, { direction: 'sendrecv' })
|
||||||
pc.addTransceiver('video', { direction: 'sendrecv' })
|
pc.addTransceiver('video', { direction: 'sendrecv' })
|
||||||
await pc.setLocalDescription(await pc.createOffer())
|
await pc.setLocalDescription(await pc.createOffer())
|
||||||
//await waitForIceGatheringComplete(pc)
|
await waitForIceGatheringComplete(pc)
|
||||||
const offer = pc.localDescription
|
const offer = pc.localDescription
|
||||||
const response = await fetch('/api/offer', {
|
const response = await fetch('/api/offer', {
|
||||||
body: JSON.stringify({ sdp: offer.sdp, type: offer.type}),
|
body: JSON.stringify({ sdp: offer.sdp, type: offer.type}),
|
||||||
@@ -57,16 +77,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const connect = async () => {
|
const connect = async () => {
|
||||||
|
_onConnecting()
|
||||||
const audioStream = await navigator.mediaDevices.getUserMedia({audio: true})
|
const audioStream = await navigator.mediaDevices.getUserMedia({audio: true})
|
||||||
peerConnection= await createSmallWebRTCConnection(audioStream.getAudioTracks()[0])
|
peerConnection= await createSmallWebRTCConnection(audioStream.getAudioTracks()[0])
|
||||||
peerConnection.onconnectionstatechange = () => {
|
}
|
||||||
let connectionState = peerConnection?.connectionState
|
|
||||||
|
const addPeerConnectionEventListeners = (pc) => {
|
||||||
|
pc.oniceconnectionstatechange = () => {
|
||||||
|
console.log("oniceconnectionstatechange", pc?.iceConnectionState)
|
||||||
|
}
|
||||||
|
pc.onconnectionstatechange = () => {
|
||||||
|
console.log("onconnectionstatechange", pc?.connectionState)
|
||||||
|
let connectionState = pc?.connectionState
|
||||||
if (connectionState === 'connected') {
|
if (connectionState === 'connected') {
|
||||||
_onConnected()
|
_onConnected()
|
||||||
} else if (connectionState === 'disconnected') {
|
} else if (connectionState === 'disconnected') {
|
||||||
_onDisconnected()
|
_onDisconnected()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pc.onicecandidate = (event) => {
|
||||||
|
if (event.candidate) {
|
||||||
|
console.log("New ICE candidate:", event.candidate);
|
||||||
|
} else {
|
||||||
|
console.log("All ICE candidates have been sent.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const _onConnecting = () => {
|
||||||
|
statusEl.textContent = "Connecting"
|
||||||
|
buttonEl.textContent = "Disconnect"
|
||||||
|
connected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const _onConnected = () => {
|
const _onConnected = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user