mirror of
https://github.com/xtr-dev/rondevu-demo.git
synced 2025-12-15 12:33:23 +00:00
Remove Node.js Host Guide
Remove NODE_HOST_GUIDE.md as requested. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,591 +0,0 @@
|
|||||||
# Hosting WebRTC Services with Node.js
|
|
||||||
|
|
||||||
This guide shows you how to create a WebRTC service host in Node.js that web clients can discover and connect to using Rondevu.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Prerequisites](#prerequisites)
|
|
||||||
- [Node.js Host (Offerer)](#nodejs-host-offerer)
|
|
||||||
- [Browser Client (Answerer)](#browser-client-answerer)
|
|
||||||
- [Message Protocol](#message-protocol)
|
|
||||||
- [WebRTC Patterns](#webrtc-patterns)
|
|
||||||
- [TURN Server Configuration](#turn-server-configuration)
|
|
||||||
- [Troubleshooting](#troubleshooting)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
In this pattern:
|
|
||||||
- **Node.js host** runs a service (e.g., chat bot, data processor) and publishes offers on Rondevu
|
|
||||||
- **Browser clients** discover the service and connect via WebRTC
|
|
||||||
- **Direct P2P communication** happens over WebRTC data channels (no server relay after connection)
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
|
|
||||||
│ Node.js Host │────1───▶│ Rondevu │◀───2────│ Browser Client │
|
|
||||||
│ (Offerer) │ │ Server │ │ (Answerer) │
|
|
||||||
│ │ │ │ │ │
|
|
||||||
│ Publishes offer │ │ Signaling │ │ Gets offer │
|
|
||||||
│ Creates channel │ │ │ │ Receives channel│
|
|
||||||
└────────┬────────┘ └──────────────┘ └────────┬────────┘
|
|
||||||
│ │
|
|
||||||
│ 3. WebRTC P2P Connection │
|
|
||||||
└─────────────────────────────────────────────────────┘
|
|
||||||
(Data channel messages)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### Node.js Requirements
|
|
||||||
|
|
||||||
- Node.js 19+ (recommended), OR
|
|
||||||
- Node.js 18 with `--experimental-global-webcrypto` flag
|
|
||||||
|
|
||||||
### Install Dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @xtr-dev/rondevu-client wrtc
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important:** `wrtc` requires native compilation and build tools:
|
|
||||||
|
|
||||||
**Ubuntu/Debian:**
|
|
||||||
```bash
|
|
||||||
sudo apt-get install python3 make g++
|
|
||||||
npm install wrtc
|
|
||||||
```
|
|
||||||
|
|
||||||
**macOS:**
|
|
||||||
```bash
|
|
||||||
# Xcode Command Line Tools required
|
|
||||||
xcode-select --install
|
|
||||||
npm install wrtc
|
|
||||||
```
|
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
```bash
|
|
||||||
# Visual Studio Build Tools required
|
|
||||||
npm install --global windows-build-tools
|
|
||||||
npm install wrtc
|
|
||||||
```
|
|
||||||
|
|
||||||
Installation may take several minutes as wrtc compiles native WebRTC libraries.
|
|
||||||
|
|
||||||
## Node.js Host (Offerer)
|
|
||||||
|
|
||||||
Here's a complete example of a Node.js service host that creates a chat bot:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
#!/usr/bin/env node
|
|
||||||
import { Rondevu, NodeCryptoAdapter } from '@xtr-dev/rondevu-client'
|
|
||||||
import wrtcModule from 'wrtc'
|
|
||||||
|
|
||||||
const { RTCPeerConnection } = wrtcModule
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const API_URL = 'https://api.ronde.vu'
|
|
||||||
const USERNAME = 'chatbot' // Your service username
|
|
||||||
const SERVICE = 'chat:2.0.0' // Service name (username will be auto-appended)
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('🤖 Starting Chat Bot Service')
|
|
||||||
console.log('='.repeat(50))
|
|
||||||
|
|
||||||
// 1. Connect to Rondevu with Node crypto adapter and ICE server preset
|
|
||||||
console.log('1. Connecting to Rondevu...')
|
|
||||||
const rondevu = await Rondevu.connect({
|
|
||||||
apiUrl: API_URL,
|
|
||||||
username: USERNAME,
|
|
||||||
cryptoAdapter: new NodeCryptoAdapter(),
|
|
||||||
iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(` ✓ Connected as: ${rondevu.getUsername()}`)
|
|
||||||
console.log(` ✓ Public key: ${rondevu.getPublicKey()?.substring(0, 20)}...`)
|
|
||||||
|
|
||||||
// 2. Publish service with automatic offer management
|
|
||||||
console.log('2. Publishing service with automatic offer management...')
|
|
||||||
|
|
||||||
await rondevu.publishService({
|
|
||||||
service: SERVICE,
|
|
||||||
maxOffers: 5, // Maintain up to 5 concurrent offers
|
|
||||||
offerFactory: async (rtcConfig) => {
|
|
||||||
console.log('\n3. Creating new WebRTC offer...')
|
|
||||||
const pc = new RTCPeerConnection(rtcConfig)
|
|
||||||
|
|
||||||
// IMPORTANT: Offerer creates the data channel
|
|
||||||
const dc = pc.createDataChannel('chat', {
|
|
||||||
ordered: true,
|
|
||||||
maxRetransmits: 3
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set up data channel handlers
|
|
||||||
dc.onopen = () => {
|
|
||||||
console.log(' ✓ Data channel opened with new peer!')
|
|
||||||
|
|
||||||
// Send welcome message
|
|
||||||
dc.send(JSON.stringify({
|
|
||||||
type: 'identify',
|
|
||||||
from: USERNAME,
|
|
||||||
publicKey: rondevu.getPublicKey()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
dc.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(event.data)
|
|
||||||
console.log(`📥 Message from peer:`, msg)
|
|
||||||
|
|
||||||
if (msg.type === 'identify') {
|
|
||||||
// Peer identified themselves
|
|
||||||
console.log(` Peer: @${msg.from}`)
|
|
||||||
|
|
||||||
// Send acknowledgment
|
|
||||||
dc.send(JSON.stringify({
|
|
||||||
type: 'identify_ack',
|
|
||||||
from: USERNAME,
|
|
||||||
publicKey: rondevu.getPublicKey()
|
|
||||||
}))
|
|
||||||
} else if (msg.type === 'message') {
|
|
||||||
// Received chat message - echo it back
|
|
||||||
console.log(` 💬 @${msg.from || 'peer'}: ${msg.text}`)
|
|
||||||
|
|
||||||
dc.send(JSON.stringify({
|
|
||||||
type: 'message',
|
|
||||||
from: USERNAME,
|
|
||||||
text: `Echo: ${msg.text}`
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to parse message:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dc.onclose = () => {
|
|
||||||
console.log(' ❌ Data channel closed')
|
|
||||||
}
|
|
||||||
|
|
||||||
dc.onerror = (error) => {
|
|
||||||
console.error(' ❌ Data channel error:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monitor connection state
|
|
||||||
pc.onconnectionstatechange = () => {
|
|
||||||
console.log(` Connection state: ${pc.connectionState}`)
|
|
||||||
if (pc.connectionState === 'connected') {
|
|
||||||
console.log(` ✅ Connected to peer!`)
|
|
||||||
} else if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
|
|
||||||
console.log(` ❌ Connection ${pc.connectionState}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pc.oniceconnectionstatechange = () => {
|
|
||||||
console.log(` ICE state: ${pc.iceConnectionState}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create offer
|
|
||||||
const offer = await pc.createOffer()
|
|
||||||
await pc.setLocalDescription(offer)
|
|
||||||
console.log(' ✓ Offer created and local description set')
|
|
||||||
|
|
||||||
return { pc, dc, offer }
|
|
||||||
},
|
|
||||||
ttl: 300000 // 5 minutes per offer
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(` ✓ Service published: ${SERVICE}@${USERNAME}`)
|
|
||||||
|
|
||||||
// 3. Start automatic offer pool management
|
|
||||||
console.log('3. Starting automatic offer pool management...')
|
|
||||||
await rondevu.startFilling()
|
|
||||||
console.log(` ✓ Maintaining up to 5 concurrent offers`)
|
|
||||||
console.log(` ✓ Polling for answers and ICE candidates`)
|
|
||||||
console.log(`\n✅ Service is live! Clients can connect to: ${SERVICE}@${USERNAME}`)
|
|
||||||
|
|
||||||
// 4. Handle graceful shutdown
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
console.log('\n\n🛑 Shutting down...')
|
|
||||||
rondevu.stopFilling()
|
|
||||||
process.exit(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(err => {
|
|
||||||
console.error('Fatal error:', err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running the Host
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Make executable
|
|
||||||
chmod +x host-service.js
|
|
||||||
|
|
||||||
# Run
|
|
||||||
node host-service.js
|
|
||||||
|
|
||||||
# Or with Node 18:
|
|
||||||
node --experimental-global-webcrypto host-service.js
|
|
||||||
```
|
|
||||||
|
|
||||||
## Browser Client (Answerer)
|
|
||||||
|
|
||||||
Here's how to connect from a browser (or see the [demo app](https://ronde.vu) for a full UI):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Rondevu } from '@xtr-dev/rondevu-client'
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const API_URL = 'https://api.ronde.vu'
|
|
||||||
const SERVICE_FQN = 'chat:2.0.0@chatbot' // Full service name with username
|
|
||||||
|
|
||||||
async function connectToService() {
|
|
||||||
console.log('🌐 Connecting to chat bot...')
|
|
||||||
|
|
||||||
// 1. Connect to Rondevu with ICE server preset
|
|
||||||
const rondevu = await Rondevu.connect({
|
|
||||||
apiUrl: API_URL,
|
|
||||||
iceServers: 'ipv4-turn' // Use same preset as host
|
|
||||||
// No username = auto-generated anonymous username
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`✓ Connected as: ${rondevu.getUsername()}`)
|
|
||||||
|
|
||||||
// 2. Connect to service (automatic WebRTC setup)
|
|
||||||
console.log(`Looking for service: ${SERVICE_FQN}`)
|
|
||||||
|
|
||||||
const connection = await rondevu.connectToService({
|
|
||||||
serviceFqn: SERVICE_FQN,
|
|
||||||
onConnection: ({ dc, peerUsername }) => {
|
|
||||||
console.log(`✅ Connected to @${peerUsername}!`)
|
|
||||||
|
|
||||||
// Set up message handler
|
|
||||||
dc.addEventListener('message', (event) => {
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(event.data)
|
|
||||||
console.log('📥 Message:', msg)
|
|
||||||
|
|
||||||
if (msg.type === 'identify') {
|
|
||||||
console.log(` Peer identified as: @${msg.from}`)
|
|
||||||
} else if (msg.type === 'identify_ack') {
|
|
||||||
console.log(' ✅ Connection acknowledged!')
|
|
||||||
|
|
||||||
// Send a test message
|
|
||||||
dc.send(JSON.stringify({
|
|
||||||
type: 'message',
|
|
||||||
text: 'Hello from browser!'
|
|
||||||
}))
|
|
||||||
} else if (msg.type === 'message') {
|
|
||||||
console.log(` 💬 @${msg.from}: ${msg.text}`)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Parse error:', err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Send identify message
|
|
||||||
dc.send(JSON.stringify({
|
|
||||||
type: 'identify',
|
|
||||||
from: rondevu.getUsername(),
|
|
||||||
publicKey: rondevu.getPublicKey()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('✅ Connection established!')
|
|
||||||
|
|
||||||
// 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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run it
|
|
||||||
connectToService().catch(err => {
|
|
||||||
console.error('Error:', err)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Message Protocol
|
|
||||||
|
|
||||||
The examples above use a simple JSON-based protocol:
|
|
||||||
|
|
||||||
### Message Types
|
|
||||||
|
|
||||||
#### 1. Identify
|
|
||||||
Sent when a peer first connects to introduce themselves.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
type: 'identify',
|
|
||||||
from: 'username',
|
|
||||||
publicKey: 'base64-encoded-public-key' // For verification
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Identify Acknowledgment
|
|
||||||
Response to identify message.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
type: 'identify_ack',
|
|
||||||
from: 'username',
|
|
||||||
publicKey: 'base64-encoded-public-key'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Chat Message
|
|
||||||
Actual message content.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
type: 'message',
|
|
||||||
from: 'username', // Optional
|
|
||||||
text: 'message text'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Protocols
|
|
||||||
|
|
||||||
You can implement any protocol you want over the data channel:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Binary protocol
|
|
||||||
dc.binaryType = 'arraybuffer'
|
|
||||||
dc.send(new Uint8Array([1, 2, 3, 4]))
|
|
||||||
|
|
||||||
// Custom JSON protocol
|
|
||||||
dc.send(JSON.stringify({
|
|
||||||
type: 'file-transfer',
|
|
||||||
filename: 'document.pdf',
|
|
||||||
size: 1024000,
|
|
||||||
chunks: 100
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
## WebRTC Patterns
|
|
||||||
|
|
||||||
### Critical Pattern: Data Channel Creation
|
|
||||||
|
|
||||||
**IMPORTANT:** In WebRTC, only the **offerer** creates data channels. The **answerer** receives them.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ✅ CORRECT - Offerer (Node.js host)
|
|
||||||
const pc = new RTCPeerConnection()
|
|
||||||
const dc = pc.createDataChannel('chat') // Offerer creates
|
|
||||||
const offer = await pc.createOffer()
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// ✅ CORRECT - Answerer (Browser client)
|
|
||||||
const pc = new RTCPeerConnection()
|
|
||||||
pc.ondatachannel = (event) => { // Answerer receives via event
|
|
||||||
const dc = event.channel
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
await pc.setRemoteDescription(offer)
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// ❌ WRONG - Answerer creating channel
|
|
||||||
const pc = new RTCPeerConnection()
|
|
||||||
const dc = pc.createDataChannel('chat') // DON'T DO THIS!
|
|
||||||
// This creates a SEPARATE channel that won't communicate
|
|
||||||
```
|
|
||||||
|
|
||||||
Creating channels on both sides results in two separate, non-communicating channels. Always follow the offerer/answerer pattern.
|
|
||||||
|
|
||||||
### ICE Candidate Timing
|
|
||||||
|
|
||||||
Set up ICE handlers **before** setting local description to catch all candidates:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ✅ CORRECT ORDER
|
|
||||||
pc.onicecandidate = (event) => {
|
|
||||||
// Send candidate to server
|
|
||||||
}
|
|
||||||
|
|
||||||
await pc.setLocalDescription(offer) // This triggers ICE gathering
|
|
||||||
|
|
||||||
// ❌ WRONG ORDER
|
|
||||||
await pc.setLocalDescription(offer) // Starts gathering immediately
|
|
||||||
|
|
||||||
pc.onicecandidate = (event) => {
|
|
||||||
// Might miss early candidates!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Answer Before ICE (Answerer)
|
|
||||||
|
|
||||||
Answerers should send their answer **before** ICE gathering to authorize candidate posting:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ✅ CORRECT - Answer first, then gather ICE
|
|
||||||
await pc.setRemoteDescription(offer)
|
|
||||||
const answer = await pc.createAnswer()
|
|
||||||
|
|
||||||
// Send answer to authorize ICE posting
|
|
||||||
await rondevu.postOfferAnswer(serviceFqn, offerId, answer.sdp)
|
|
||||||
|
|
||||||
// Now set local description (starts ICE gathering)
|
|
||||||
await pc.setLocalDescription(answer)
|
|
||||||
|
|
||||||
// ICE candidates can now be posted (authorized)
|
|
||||||
```
|
|
||||||
|
|
||||||
## TURN Server Configuration
|
|
||||||
|
|
||||||
For production deployments, you'll need TURN servers for NAT traversal:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const RTC_CONFIG = {
|
|
||||||
iceServers: [
|
|
||||||
// STUN for public IP discovery
|
|
||||||
{ urls: 'stun:stun.l.google.com:19302' },
|
|
||||||
|
|
||||||
// TURN relay for NAT traversal
|
|
||||||
{
|
|
||||||
urls: [
|
|
||||||
'turn:your-turn-server.com:3478?transport=tcp',
|
|
||||||
'turn:your-turn-server.com:3478?transport=udp',
|
|
||||||
],
|
|
||||||
username: 'your-username',
|
|
||||||
credential: 'your-password'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing TURN Connectivity
|
|
||||||
|
|
||||||
Use `turnutils_uclient` to verify TURN server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install coturn utilities
|
|
||||||
sudo apt-get install coturn-utils
|
|
||||||
|
|
||||||
# Test TURN server
|
|
||||||
turnutils_uclient -u username -w password your-turn-server.com 3478 -y
|
|
||||||
```
|
|
||||||
|
|
||||||
### Force TURN (Testing)
|
|
||||||
|
|
||||||
To test if TURN is working, force relay mode:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const RTC_CONFIG = {
|
|
||||||
iceServers: [/* ... */],
|
|
||||||
iceTransportPolicy: 'relay' // Forces TURN, bypasses direct connections
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove** `iceTransportPolicy: 'relay'` for production to allow direct connections when possible.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Connection Stuck in "connecting"
|
|
||||||
|
|
||||||
**Possible causes:**
|
|
||||||
1. TURN server not working
|
|
||||||
2. Both peers behind same NAT (hairpinning issue)
|
|
||||||
3. Firewall blocking UDP ports
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
```javascript
|
|
||||||
// Enable relay-only mode to test TURN
|
|
||||||
const RTC_CONFIG = {
|
|
||||||
iceServers: [/* ... */],
|
|
||||||
iceTransportPolicy: 'relay'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check TURN server
|
|
||||||
turnutils_uclient -u user -w pass server.com 3478 -y
|
|
||||||
|
|
||||||
// Verify both peers are on different networks
|
|
||||||
```
|
|
||||||
|
|
||||||
### No Candidates Gathered
|
|
||||||
|
|
||||||
**Possible causes:**
|
|
||||||
1. ICE handler set up too late
|
|
||||||
2. STUN/TURN servers unreachable
|
|
||||||
3. Firewall blocking
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
```javascript
|
|
||||||
// Set handler BEFORE setLocalDescription
|
|
||||||
pc.onicecandidate = (event) => { /* ... */ }
|
|
||||||
await pc.setLocalDescription(offer)
|
|
||||||
|
|
||||||
// Test STUN connectivity
|
|
||||||
ping stun.l.google.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Messages Not Received
|
|
||||||
|
|
||||||
**Possible causes:**
|
|
||||||
1. Data channel created on both sides
|
|
||||||
2. Channel not opened yet
|
|
||||||
3. Wrong channel name
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
```javascript
|
|
||||||
// Only offerer creates channel
|
|
||||||
// Offerer:
|
|
||||||
const dc = pc.createDataChannel('chat')
|
|
||||||
|
|
||||||
// Answerer:
|
|
||||||
pc.ondatachannel = (event) => {
|
|
||||||
const dc = event.channel // Receive it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for channel to open
|
|
||||||
dc.onopen = () => {
|
|
||||||
dc.send('message') // Now safe to send
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### wrtc Installation Fails
|
|
||||||
|
|
||||||
**Ubuntu/Debian:**
|
|
||||||
```bash
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y python3 make g++ pkg-config libssl-dev
|
|
||||||
npm install wrtc
|
|
||||||
```
|
|
||||||
|
|
||||||
**macOS:**
|
|
||||||
```bash
|
|
||||||
xcode-select --install
|
|
||||||
npm install wrtc
|
|
||||||
```
|
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
```bash
|
|
||||||
npm install --global windows-build-tools
|
|
||||||
npm install wrtc
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Working Example
|
|
||||||
|
|
||||||
See `/demo/test-connect.js` for a complete working example that connects to the chat demo at `chat:2.0.0@bas`.
|
|
||||||
|
|
||||||
To run:
|
|
||||||
```bash
|
|
||||||
cd demo
|
|
||||||
npm install wrtc
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
- [Rondevu Client API](../client/README.md)
|
|
||||||
- [WebRTC MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API)
|
|
||||||
- [wrtc GitHub](https://github.com/node-webrtc/node-webrtc)
|
|
||||||
- [TURN Server Setup (coturn)](https://github.com/coturn/coturn)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
Reference in New Issue
Block a user