VANI वाणी

Voice And Networked Interaction

WebRTC voice and video calling for the Yakmesh network.

v1.0.0

Architecture

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');
};