mirror of
https://github.com/xtr-dev/rondevu-demo.git
synced 2025-12-15 12:33:23 +00:00
Update NODE_HOST_GUIDE to use automatic API
- Replace manual WebRTC setup with offerFactory pattern - Use publishService() + startFilling() instead of manual polling - Update browser client to use connectToService() - Remove all manual ICE candidate polling code - Simplify shutdown to just call stopFilling() - Reduce example from ~400 lines to ~150 lines
This commit is contained in:
@@ -87,22 +87,6 @@ const API_URL = 'https://api.ronde.vu'
|
|||||||
const USERNAME = 'chatbot' // Your service username
|
const USERNAME = 'chatbot' // Your service username
|
||||||
const SERVICE = 'chat:2.0.0' // Service name (username will be auto-appended)
|
const SERVICE = 'chat:2.0.0' // Service name (username will be auto-appended)
|
||||||
|
|
||||||
// TURN server configuration for manual RTCPeerConnection setup
|
|
||||||
// Note: If using automatic offer management, configure via Rondevu.connect() iceServers option
|
|
||||||
const RTC_CONFIG = {
|
|
||||||
iceServers: [
|
|
||||||
{ urls: 'stun:stun.l.google.com:19302' },
|
|
||||||
{
|
|
||||||
urls: [
|
|
||||||
'turn:57.129.61.67:3478?transport=tcp',
|
|
||||||
'turn:57.129.61.67:3478?transport=udp',
|
|
||||||
],
|
|
||||||
username: 'webrtcuser',
|
|
||||||
credential: 'supersecretpassword'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('🤖 Starting Chat Bot Service')
|
console.log('🤖 Starting Chat Bot Service')
|
||||||
console.log('='.repeat(50))
|
console.log('='.repeat(50))
|
||||||
@@ -119,16 +103,15 @@ async function main() {
|
|||||||
console.log(` ✓ Connected as: ${rondevu.getUsername()}`)
|
console.log(` ✓ Connected as: ${rondevu.getUsername()}`)
|
||||||
console.log(` ✓ Public key: ${rondevu.getPublicKey()?.substring(0, 20)}...`)
|
console.log(` ✓ Public key: ${rondevu.getPublicKey()?.substring(0, 20)}...`)
|
||||||
|
|
||||||
// 2. Username will be auto-claimed on first authenticated request (publishService)
|
// 2. Publish service with automatic offer management
|
||||||
console.log('2. Username will be auto-claimed on first publish...')
|
console.log('2. Publishing service with automatic offer management...')
|
||||||
|
|
||||||
// Keep track of active connections
|
await rondevu.publishService({
|
||||||
const connections = new Map()
|
service: SERVICE,
|
||||||
|
maxOffers: 5, // Maintain up to 5 concurrent offers
|
||||||
// 3. Create connection handler for new peers
|
offerFactory: async (rtcConfig) => {
|
||||||
async function createOffer() {
|
|
||||||
console.log('\n3. Creating new WebRTC offer...')
|
console.log('\n3. Creating new WebRTC offer...')
|
||||||
const pc = new RTCPeerConnection(RTC_CONFIG)
|
const pc = new RTCPeerConnection(rtcConfig)
|
||||||
|
|
||||||
// IMPORTANT: Offerer creates the data channel
|
// IMPORTANT: Offerer creates the data channel
|
||||||
const dc = pc.createDataChannel('chat', {
|
const dc = pc.createDataChannel('chat', {
|
||||||
@@ -186,57 +169,13 @@ async function main() {
|
|||||||
console.error(' ❌ Data channel error:', error)
|
console.error(' ❌ Data channel error:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Create offer
|
// Monitor connection state
|
||||||
const offer = await pc.createOffer()
|
|
||||||
await pc.setLocalDescription(offer)
|
|
||||||
console.log(' ✓ Local description set')
|
|
||||||
|
|
||||||
// 5. Publish service with offer
|
|
||||||
console.log('4. Publishing service to Rondevu...')
|
|
||||||
const result = await rondevu.publishService({
|
|
||||||
service: SERVICE,
|
|
||||||
offers: [{ sdp: offer.sdp }],
|
|
||||||
ttl: 300000 // 5 minutes
|
|
||||||
})
|
|
||||||
|
|
||||||
const offerId = result.offers[0].offerId
|
|
||||||
const serviceFqn = result.serviceFqn // Full FQN with username
|
|
||||||
console.log(` ✓ Service published with offer ID: ${offerId}`)
|
|
||||||
|
|
||||||
// Store connection info
|
|
||||||
connections.set(offerId, { pc, dc, answered: false })
|
|
||||||
|
|
||||||
// 6. Set up ICE candidate handler BEFORE candidates are gathered
|
|
||||||
pc.onicecandidate = async (event) => {
|
|
||||||
if (event.candidate) {
|
|
||||||
console.log(' 📤 Sending ICE candidate')
|
|
||||||
try {
|
|
||||||
// wrtc doesn't have toJSON, manually serialize
|
|
||||||
const candidateInit = {
|
|
||||||
candidate: event.candidate.candidate,
|
|
||||||
sdpMLineIndex: event.candidate.sdpMLineIndex,
|
|
||||||
sdpMid: event.candidate.sdpMid,
|
|
||||||
usernameFragment: event.candidate.usernameFragment
|
|
||||||
}
|
|
||||||
await rondevu.getAPIPublic().addOfferIceCandidates(
|
|
||||||
serviceFqn,
|
|
||||||
offerId,
|
|
||||||
[candidateInit]
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to send ICE candidate:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Monitor connection state
|
|
||||||
pc.onconnectionstatechange = () => {
|
pc.onconnectionstatechange = () => {
|
||||||
console.log(` Connection state: ${pc.connectionState}`)
|
console.log(` Connection state: ${pc.connectionState}`)
|
||||||
if (pc.connectionState === 'connected') {
|
if (pc.connectionState === 'connected') {
|
||||||
console.log(` ✅ Connected to peer via offer ${offerId}`)
|
console.log(` ✅ Connected to peer!`)
|
||||||
} else if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
|
} else if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
|
||||||
console.log(` ❌ Connection ${pc.connectionState} for offer ${offerId}`)
|
console.log(` ❌ Connection ${pc.connectionState}`)
|
||||||
connections.delete(offerId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,70 +183,29 @@ async function main() {
|
|||||||
console.log(` ICE state: ${pc.iceConnectionState}`)
|
console.log(` ICE state: ${pc.iceConnectionState}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return offerId
|
// Create offer
|
||||||
}
|
const offer = await pc.createOffer()
|
||||||
|
await pc.setLocalDescription(offer)
|
||||||
|
console.log(' ✓ Offer created and local description set')
|
||||||
|
|
||||||
// 8. Poll for answers and ICE candidates
|
return { pc, dc, offer }
|
||||||
console.log('5. Starting to poll for answers...')
|
},
|
||||||
let lastPollTimestamp = 0
|
ttl: 300000 // 5 minutes per offer
|
||||||
|
})
|
||||||
|
|
||||||
const pollInterval = setInterval(async () => {
|
console.log(` ✓ Service published: ${SERVICE}@${USERNAME}`)
|
||||||
try {
|
|
||||||
const result = await rondevu.pollOffers(lastPollTimestamp)
|
|
||||||
|
|
||||||
// Process answers
|
// 3. Start automatic offer pool management
|
||||||
for (const answer of result.answers) {
|
console.log('3. Starting automatic offer pool management...')
|
||||||
const conn = connections.get(answer.offerId)
|
await rondevu.startFilling()
|
||||||
if (conn && !conn.answered) {
|
console.log(` ✓ Maintaining up to 5 concurrent offers`)
|
||||||
console.log(`\n📥 Received answer for offer ${answer.offerId}`)
|
console.log(` ✓ Polling for answers and ICE candidates`)
|
||||||
await conn.pc.setRemoteDescription({ type: 'answer', sdp: answer.sdp })
|
console.log(`\n✅ Service is live! Clients can connect to: ${SERVICE}@${USERNAME}`)
|
||||||
conn.answered = true
|
|
||||||
lastPollTimestamp = answer.answeredAt
|
|
||||||
|
|
||||||
// Create new offer for next peer
|
// 4. Handle graceful shutdown
|
||||||
await createOffer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process ICE candidates
|
|
||||||
for (const [offerId, candidates] of Object.entries(result.iceCandidates)) {
|
|
||||||
const conn = connections.get(offerId)
|
|
||||||
if (conn) {
|
|
||||||
const answererCandidates = candidates.filter(c => c.role === 'answerer')
|
|
||||||
|
|
||||||
for (const item of answererCandidates) {
|
|
||||||
if (item.candidate) {
|
|
||||||
console.log(` 📥 Received ICE candidate for offer ${offerId}`)
|
|
||||||
await conn.pc.addIceCandidate(item.candidate)
|
|
||||||
lastPollTimestamp = Math.max(lastPollTimestamp, item.createdAt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Polling error:', err.message)
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
// 9. Create initial offer
|
|
||||||
await createOffer()
|
|
||||||
|
|
||||||
console.log('\n✅ Service is live! Waiting for connections...')
|
|
||||||
console.log(` Service: ${SERVICE}`)
|
|
||||||
console.log(` Username: ${USERNAME}`)
|
|
||||||
console.log(` Clients can connect by discovering: ${SERVICE}@${USERNAME}`)
|
|
||||||
|
|
||||||
// Handle graceful shutdown
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.log('\n\n🛑 Shutting down...')
|
console.log('\n\n🛑 Shutting down...')
|
||||||
clearInterval(pollInterval)
|
rondevu.stopFilling()
|
||||||
|
|
||||||
for (const [offerId, conn] of connections.entries()) {
|
|
||||||
console.log(` Closing connection ${offerId}`)
|
|
||||||
conn.dc?.close()
|
|
||||||
conn.pc?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -342,67 +240,34 @@ import { Rondevu } from '@xtr-dev/rondevu-client'
|
|||||||
const API_URL = 'https://api.ronde.vu'
|
const API_URL = 'https://api.ronde.vu'
|
||||||
const SERVICE_FQN = 'chat:2.0.0@chatbot' // Full service name with username
|
const SERVICE_FQN = 'chat:2.0.0@chatbot' // Full service name with username
|
||||||
|
|
||||||
// TURN server configuration for manual RTCPeerConnection setup
|
|
||||||
const RTC_CONFIG = {
|
|
||||||
iceServers: [
|
|
||||||
{ urls: 'stun:stun.l.google.com:19302' },
|
|
||||||
{
|
|
||||||
urls: [
|
|
||||||
'turn:57.129.61.67:3478?transport=tcp',
|
|
||||||
'turn:57.129.61.67:3478?transport=udp',
|
|
||||||
],
|
|
||||||
username: 'webrtcuser',
|
|
||||||
credential: 'supersecretpassword'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectToService() {
|
async function connectToService() {
|
||||||
console.log('🌐 Connecting to chat bot...')
|
console.log('🌐 Connecting to chat bot...')
|
||||||
|
|
||||||
// 1. Connect to Rondevu (anonymous user with ICE server preset)
|
// 1. Connect to Rondevu with ICE server preset
|
||||||
const rondevu = await Rondevu.connect({
|
const rondevu = await Rondevu.connect({
|
||||||
apiUrl: API_URL,
|
apiUrl: API_URL,
|
||||||
iceServers: 'ipv4-turn', // Use preset or custom config
|
iceServers: 'ipv4-turn' // Use same preset as host
|
||||||
// No username = auto-generated anonymous username
|
// No username = auto-generated anonymous username
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`✓ Connected as: ${rondevu.getUsername()}`)
|
console.log(`✓ Connected as: ${rondevu.getUsername()}`)
|
||||||
|
|
||||||
// 2. Discover service
|
// 2. Connect to service (automatic WebRTC setup)
|
||||||
console.log(`Looking for service: ${SERVICE_FQN}`)
|
console.log(`Looking for service: ${SERVICE_FQN}`)
|
||||||
const serviceData = await rondevu.getService(SERVICE_FQN)
|
|
||||||
console.log(`✓ Found service from @${serviceData.username}`)
|
|
||||||
|
|
||||||
// 3. Create peer connection
|
const connection = await rondevu.connectToService({
|
||||||
const pc = new RTCPeerConnection(RTC_CONFIG)
|
serviceFqn: SERVICE_FQN,
|
||||||
|
onConnection: ({ dc, peerUsername }) => {
|
||||||
|
console.log(`✅ Connected to @${peerUsername}!`)
|
||||||
|
|
||||||
// 4. IMPORTANT: Answerer receives data channel via ondatachannel
|
// Set up message handler
|
||||||
// DO NOT create a channel with pc.createDataChannel()
|
dc.addEventListener('message', (event) => {
|
||||||
let dc = null
|
|
||||||
|
|
||||||
pc.ondatachannel = (event) => {
|
|
||||||
console.log('✓ Data channel received from host!')
|
|
||||||
dc = event.channel
|
|
||||||
|
|
||||||
dc.onopen = () => {
|
|
||||||
console.log('✓ Data channel opened!')
|
|
||||||
|
|
||||||
// Send identify message
|
|
||||||
dc.send(JSON.stringify({
|
|
||||||
type: 'identify',
|
|
||||||
from: rondevu.getUsername(),
|
|
||||||
publicKey: rondevu.getPublicKey()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
dc.onmessage = (event) => {
|
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data)
|
const msg = JSON.parse(event.data)
|
||||||
console.log('📥 Message:', msg)
|
console.log('📥 Message:', msg)
|
||||||
|
|
||||||
if (msg.type === 'identify') {
|
if (msg.type === 'identify') {
|
||||||
console.log(`Connected to @${msg.from}`)
|
console.log(` Peer identified as: @${msg.from}`)
|
||||||
} else if (msg.type === 'identify_ack') {
|
} else if (msg.type === 'identify_ack') {
|
||||||
console.log(' ✅ Connection acknowledged!')
|
console.log(' ✅ Connection acknowledged!')
|
||||||
|
|
||||||
@@ -417,95 +282,26 @@ async function connectToService() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Parse error:', err)
|
console.error('Parse error:', err)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
dc.onclose = () => {
|
// Send identify message
|
||||||
console.log('❌ Data channel closed')
|
dc.send(JSON.stringify({
|
||||||
|
type: 'identify',
|
||||||
|
from: rondevu.getUsername(),
|
||||||
|
publicKey: rondevu.getPublicKey()
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
dc.onerror = (error) => {
|
console.log('✅ Connection established!')
|
||||||
console.error('❌ Data channel error:', error)
|
|
||||||
|
// Monitor connection state
|
||||||
|
connection.pc.onconnectionstatechange = () => {
|
||||||
|
console.log(`Connection state: ${connection.pc.connectionState}`)
|
||||||
|
if (connection.pc.connectionState === 'failed' || connection.pc.connectionState === 'closed') {
|
||||||
|
console.log('❌ Connection ended')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Set up ICE candidate handler BEFORE setting remote description
|
|
||||||
pc.onicecandidate = async (event) => {
|
|
||||||
if (event.candidate) {
|
|
||||||
console.log('📤 Sending ICE candidate')
|
|
||||||
try {
|
|
||||||
await rondevu.getAPIPublic().addOfferIceCandidates(
|
|
||||||
serviceData.serviceFqn,
|
|
||||||
serviceData.offerId,
|
|
||||||
[event.candidate.toJSON()]
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to send ICE candidate:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Set remote offer
|
|
||||||
console.log('Setting remote offer...')
|
|
||||||
await pc.setRemoteDescription({ type: 'offer', sdp: serviceData.sdp })
|
|
||||||
|
|
||||||
// 7. Create and set local answer
|
|
||||||
console.log('Creating answer...')
|
|
||||||
const answer = await pc.createAnswer()
|
|
||||||
await pc.setLocalDescription(answer)
|
|
||||||
|
|
||||||
// 8. Send answer to server
|
|
||||||
console.log('Sending answer...')
|
|
||||||
await rondevu.postOfferAnswer(
|
|
||||||
serviceData.serviceFqn,
|
|
||||||
serviceData.offerId,
|
|
||||||
answer.sdp
|
|
||||||
)
|
|
||||||
|
|
||||||
// 9. Poll for remote ICE candidates
|
|
||||||
console.log('Polling for ICE candidates...')
|
|
||||||
let lastIceTimestamp = 0
|
|
||||||
|
|
||||||
const pollInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const result = await rondevu.getOfferIceCandidates(
|
|
||||||
serviceData.serviceFqn,
|
|
||||||
serviceData.offerId,
|
|
||||||
lastIceTimestamp
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const item of result.candidates) {
|
|
||||||
if (item.candidate) {
|
|
||||||
console.log('📥 Received ICE candidate')
|
|
||||||
await pc.addIceCandidate(new RTCIceCandidate(item.candidate))
|
|
||||||
lastIceTimestamp = item.createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('ICE polling error:', err)
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
// 10. Monitor connection state
|
|
||||||
pc.onconnectionstatechange = () => {
|
|
||||||
console.log(`Connection state: ${pc.connectionState}`)
|
|
||||||
|
|
||||||
if (pc.connectionState === 'connected') {
|
|
||||||
console.log('✅ Successfully connected!')
|
|
||||||
clearInterval(pollInterval)
|
|
||||||
} else if (pc.connectionState === 'failed') {
|
|
||||||
console.error('❌ Connection failed')
|
|
||||||
clearInterval(pollInterval)
|
|
||||||
} else if (pc.connectionState === 'closed') {
|
|
||||||
console.log('Connection closed')
|
|
||||||
clearInterval(pollInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pc.oniceconnectionstatechange = () => {
|
|
||||||
console.log(`ICE state: ${pc.iceConnectionState}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('⏳ Waiting for connection...')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run it
|
// Run it
|
||||||
|
|||||||
Reference in New Issue
Block a user