VANI वाणी
Voice And Networked Interaction
WebRTC voice and video calling for the Yakmesh network.
v1.0.0Architecture
VANI provides real-time voice and video communication using WebRTC for media transport and the Yakmesh mesh network for signaling:
┌──────────────────────────────────────────────────────────────────┐
│ VANI ARCHITECTURE │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ Mesh Signaling ┌─────────────┐ │
│ │ Alice │ ◄────── SDP, ICE ────────────► │ Bob │ │
│ │ VaniHub │ OFFER/ANSWER │ VaniHub │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ │ WebRTC P2P │ │
│ └──────────── Media Stream ──────────────────────┘ │
│ (Audio/Video/Screen) │
│ │
├──────────────────────────────────────────────────────────────────┤
│ Key Benefits: │
│ • No central media server - P2P encrypted streams │
│ • Signaling via mesh - no STUN/TURN dependency for setup │
│ • GUMBA integration - private calls in access-controlled rooms │
│ • Multi-party support - group calls up to 10 participants │
└──────────────────────────────────────────────────────────────────┘
How It Works
1. Call Initiation
Caller sends a CALL_OFFER signal through the mesh network with media preferences and caller info.
2. Call Answer
Callee accepts with CALL_ANSWER or rejects with CALL_REJECT. Both signals route through mesh.
3. WebRTC Negotiation
SDP_OFFER and SDP_ANSWER exchange session descriptions. ICE_CANDIDATE signals help establish connectivity.
4. Media Streaming
Once ICE completes, WebRTC creates a direct P2P connection for encrypted audio/video streams.
Quick Start
import { VaniHub, MEDIA_TYPE, CALL_STATE } from 'yakmesh/mesh/vani.js';
// Create hub with local peer ID
const hub = new VaniHub({
localPeerId: meshNode.id,
onSignal: (signal) => meshNode.broadcast(signal)
});
// Handle incoming mesh signals
meshNode.on('message', (msg) => {
if (msg.type?.startsWith('vani:')) {
hub.handleSignal(msg);
}
});
// Listen for incoming calls
hub.on('incomingCall', ({ call }) => {
console.log('Incoming call from:', call.participants.keys());
// Show UI to accept/reject
});
// Start a video call
const call = await hub.startCall({
targetPeerIds: 'peer-bob-123',
mediaType: [MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO]
});
// Handle remote streams
hub.on('remoteStream', ({ peerId, stream }) => {
const video = document.getElementById('remoteVideo');
video.srcObject = stream;
});
Call States
| State | Description | Next States |
|---|---|---|
idle |
No active call | initiating, incoming |
initiating |
Creating call offer | ringing |
ringing |
Waiting for callee to answer | connecting, ended |
incoming |
Received call, awaiting user action | connecting, ended |
connecting |
Exchanging SDP and ICE candidates | connected, failed |
connected |
Media streaming active | reconnecting, ended |
reconnecting |
Connection temporarily lost | connected, failed |
ended |
Call terminated normally | idle |
failed |
Call failed to connect | idle |
Signal Types
| Signal | Purpose | Payload |
|---|---|---|
vani:call:offer |
Initiate a call | mediaType, displayName, groupCall, bundleId |
vani:call:answer |
Accept a call | displayName, mediaType |
vani:call:reject |
Decline a call | reason |
vani:call:end |
Terminate a call | reason |
vani:call:busy |
Callee is in another call | - |
vani:sdp:offer |
WebRTC SDP offer | sdp |
vani:sdp:answer |
WebRTC SDP answer | sdp |
vani:ice:candidate |
ICE candidate for NAT traversal | candidate, sdpMid, sdpMLineIndex |
vani:mute:audio |
Audio mute state change | muted |
vani:mute:video |
Video mute state change | muted |
API Reference
VaniHub
Multi-call manager - the main entry point for voice/video.
// Constructor
new VaniHub({
localPeerId: string, // Your mesh node ID
iceServers?: RTCIceServer[], // Optional custom STUN/TURN servers
onSignal: (signal) => void // Callback to send signals via mesh
})
// Methods
hub.startCall(options) → Promise<VaniCall> // Start outgoing call
hub.handleSignal(signal) // Process incoming signal
hub.acceptCall(callId) → Promise<VaniCall> // Accept incoming call
hub.rejectCall(callId, reason?) // Reject incoming call
hub.endCall(callId, reason?) // End active call
hub.getCall(callId) → VaniCall | null // Get call by ID
hub.getActiveCall() → VaniCall | null // Get current active call
hub.getStats() → object // Get hub statistics
hub.cleanup() // Clean up all resources
// Events
hub.on('incomingCall', ({ call, signal }) => {}) // New incoming call
hub.on('stateChange', ({ callId, state }) => {}) // Call state changed
hub.on('remoteStream', ({ peerId, stream }) => {})// Remote media stream
hub.on('participantJoin', ({ participant }) => {})// Participant joined
hub.on('participantLeave', ({ participant }) => {})// Participant left
hub.on('error', ({ callId, code, error }) => {}) // Error occurred
VaniCall
Individual call session with WebRTC management.
// Properties
call.id // Unique call ID
call.state // Current call state
call.isInitiator // true if we started the call
call.isGroupCall // true if multi-party call
call.bundleId // GUMBA bundle ID for private calls
call.mediaType // Array of MEDIA_TYPE values
call.participants// Map of peerId → VaniParticipant
call.localStream // Local MediaStream
call.createdAt // Timestamp when call was created
call.connectedAt // Timestamp when call connected (null if not yet)
call.duration // Call duration in ms
// Methods
call.initiate(peerIds) // Start call to peer(s)
call.accept(callerPeerId) // Accept incoming call
call.reject(reason?) // Reject incoming call
call.end(reason?) // Hang up
call.handleSignal(signal) // Process signaling message
call.setAudioEnabled(bool) // Mute/unmute audio
call.setVideoEnabled(bool) // Enable/disable video
call.cleanup() // Release all resources
call.getDuration() → number// Get call duration
call.toJSON() → object // Serialize call info
VaniSignal
Signaling message builders.
// Static factory methods
VaniSignal.offer(options) // Create call offer
VaniSignal.answer(options) // Create call answer
VaniSignal.reject(options) // Create call rejection
VaniSignal.end(options) // Create call end
VaniSignal.sdpOffer(options) // Create SDP offer
VaniSignal.sdpAnswer(options) // Create SDP answer
VaniSignal.iceCandidate(options)// Create ICE candidate
// Instance methods
signal.validate() → { valid, errors } // Validate signal
signal.toJSON() → object // Serialize
VaniSignal.fromJSON(json) // Deserialize
VaniParticipant
Represents a participant in a call.
// Properties
participant.id // Unique participant ID
participant.peerId // Mesh peer ID
participant.displayName // Display name
participant.joinedAt // When they joined
participant.audioEnabled // Audio is on
participant.videoEnabled // Video is on
participant.screenSharing // Screen sharing active
participant.connectionState // WebRTC connection state
participant.remoteStream // Their MediaStream
Group Calls
VANI supports multi-party calls with up to 10 participants using mesh topology.
// Start a group call
const call = await hub.startCall({
targetPeerIds: ['peer-alice', 'peer-bob', 'peer-charlie'],
mediaType: [MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO],
isGroupCall: true
});
// Track participants
hub.on('participantJoin', ({ callId, participant }) => {
console.log(participant.displayName, 'joined');
});
hub.on('participantLeave', ({ callId, participant, reason }) => {
console.log(participant.displayName, 'left:', reason);
});
// Each participant has their own stream
hub.on('remoteStream', ({ callId, peerId, stream }) => {
// Create video element for this participant
const video = document.createElement('video');
video.id = 'video-' + peerId;
video.srcObject = stream;
video.autoplay = true;
document.getElementById('participants').appendChild(video);
});
Private Room Calls (GUMBA)
VANI integrates with GUMBA for access-controlled voice/video in private rooms.
// Start a call in a private GUMBA room
const call = await hub.startCall({
targetPeerIds: roomMemberPeerIds,
bundleId: gumbaBundle.id, // Links call to GUMBA access control
mediaType: [MEDIA_TYPE.AUDIO]
});
// The bundleId is included in CALL_OFFER signals
// Receivers can verify membership before accepting
call.bundleId // → 'gumba-bundle-123'
Configuration
import { VANI_CONFIG } from 'yakmesh/mesh/vani.js';
// Default configuration
VANI_CONFIG.callTimeout // 30000ms - ring timeout
VANI_CONFIG.iceGatheringTimeout// 10000ms - ICE gathering
VANI_CONFIG.reconnectTimeout // 15000ms - reconnection window
VANI_CONFIG.maxParticipants // 10 - max group size
// Default STUN servers
VANI_CONFIG.iceServers // Google STUN servers
// Custom STUN/TURN servers
const hub = new VaniHub({
localPeerId: myId,
iceServers: [
{ urls: 'stun:stun.example.com:3478' },
{
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'pass'
}
],
onSignal: sendViaMesh
});
// Media constraints
VANI_CONFIG.defaultConstraints.audio.echoCancellation // true
VANI_CONFIG.defaultConstraints.audio.noiseSuppression // true
VANI_CONFIG.defaultConstraints.video.width.ideal // 1280
VANI_CONFIG.defaultConstraints.video.height.ideal // 720
VANI_CONFIG.defaultConstraints.video.frameRate.ideal // 30
Error Handling
hub.on('error', ({ callId, code, error }) => {
switch (code) {
case 'MEDIA_ACCESS_DENIED':
// User denied camera/microphone access
alert('Please allow camera and microphone access');
break;
case 'SDP_CREATE_FAILED':
// WebRTC SDP creation failed
console.error('Failed to create offer/answer:', error);
break;
case 'SDP_NEGOTIATION_FAILED':
// SDP exchange failed
console.error('Connection negotiation failed:', error);
break;
case 'SDP_ANSWER_FAILED':
// Failed to set remote SDP answer
console.error('Failed to process answer:', error);
break;
}
});
// Call end reasons
import { CALL_END_REASON } from 'yakmesh/mesh/vani.js';
CALL_END_REASON.NORMAL // Normal hangup
CALL_END_REASON.REJECTED // Callee rejected
CALL_END_REASON.BUSY // Callee in another call
CALL_END_REASON.TIMEOUT // No answer (30s)
CALL_END_REASON.FAILED // Connection failed
CALL_END_REASON.NETWORK_ERROR // Network issue
CALL_END_REASON.PARTICIPANT_LEFT // Group participant left
NAT Traversal
VANI uses ICE (Interactive Connectivity Establishment) for NAT traversal with three connection strategies:
- Direct P2P: When both peers are on public IPs or same LAN
- STUN: Discovers public IP and port mapping for NAT punch-through
- Mesh Relay (preferred): Fallback to Yakmesh mesh network for media relay — no external servers needed
- TURN: Optional external relay for symmetric NAT (configure your own if needed)
Ethos Note: The default configuration uses empty ICE servers, relying on mesh relay instead of external STUN/TURN. For hybrid deployments, you may optionally configure your own STUN/TURN servers — but Yakmesh's mesh relay is the recommended, fully decentralized approach.
Complete Example
import { VaniHub, MEDIA_TYPE, CALL_STATE, CALL_END_REASON } from 'yakmesh/mesh/vani.js';
class CallManager {
constructor(meshNode) {
this.meshNode = meshNode;
this.hub = new VaniHub({
localPeerId: meshNode.id,
onSignal: (signal) => meshNode.send(signal.toPeer, signal)
});
this.setupEventHandlers();
this.setupMeshHandlers();
}
setupEventHandlers() {
// Incoming call notification
this.hub.on('incomingCall', ({ call }) => {
const caller = Array.from(call.participants.values())[0];
this.showIncomingCallUI(call.id, caller.displayName);
});
// State changes
this.hub.on('stateChange', ({ callId, state }) => {
this.updateCallUI(callId, state);
});
// Remote video
this.hub.on('remoteStream', ({ peerId, stream }) => {
this.attachRemoteStream(peerId, stream);
});
}
setupMeshHandlers() {
this.meshNode.on('message', (msg) => {
if (msg.type?.startsWith('vani:')) {
this.hub.handleSignal(msg);
}
});
}
async startVideoCall(peerId) {
const call = await this.hub.startCall({
targetPeerIds: peerId,
mediaType: [MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO]
});
// Attach local preview
const preview = document.getElementById('localVideo');
preview.srcObject = call.localStream;
return call;
}
async acceptCall(callId) {
const call = await this.hub.acceptCall(callId);
document.getElementById('localVideo').srcObject = call.localStream;
}
rejectCall(callId) {
this.hub.rejectCall(callId, CALL_END_REASON.REJECTED);
}
hangUp() {
const call = this.hub.getActiveCall();
if (call) {
this.hub.endCall(call.id);
}
}
toggleMute() {
const call = this.hub.getActiveCall();
if (call) {
const audioTrack = call.localStream?.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
call.setAudioEnabled(audioTrack.enabled);
}
}
}
attachRemoteStream(peerId, stream) {
let video = document.getElementById('remote-' + peerId);
if (!video) {
video = document.createElement('video');
video.id = 'remote-' + peerId;
video.autoplay = true;
document.getElementById('remoteVideos').appendChild(video);
}
video.srcObject = stream;
}
}
// Usage
const meshNode = await createMeshNode();
const callManager = new CallManager(meshNode);
// Start a call
document.getElementById('callBtn').onclick = () => {
callManager.startVideoCall('peer-bob-123');
};