wavecall-sdk
v1.0.0
Published
Monorepo for @wavecall/server and @wavecall/client
Readme
wavecall-sdk
Self-hosted WebRTC audio/video calling SDK — no Agora, no Twilio, zero third-party services.
Two packages:
| Package | Install | Use in |
|---|---|---|
| @wavecall/server | npm i @wavecall/server | Node.js + Socket.IO backend |
| @wavecall/client | npm i @wavecall/client | Browser (React, Vue, vanilla JS) |
How it works
Your server only passes tiny signaling messages (SDP + ICE). The actual audio/video flows directly peer-to-peer between browsers — your server never touches media.
Client A ──── wc:offer ──────► Your Server ──── wc:offer ──────► Client B
Client A ◄─── wc:answer ───── Your Server ◄─── wc:answer ───── Client B
◄══════════ Direct WebRTC audio/video ══════════►Server Setup
1. Install
npm install @wavecall/server2. Add to .env
WAVECALL_SECRET=any_long_random_string_here3. Add to your Socket.IO connection handler
const { WaveCallSignaling } = require('@wavecall/server');
const signaling = new WaveCallSignaling({
secret: process.env.WAVECALL_SECRET,
expiresIn: 3600, // token TTL in seconds (optional)
onCallStarted: ({ callId, roomId, callType }) => {
// optional: persist to your DB here
},
onCallEnded: ({ callId, roomId, reason }) => {
// optional: update DB, send notifications, etc.
},
});
// Inside your existing io.on('connection') — add ONE line:
io.on('connection', (socket) => {
const { userId, name } = socket.user; // from your auth middleware
handleChatMessageEvents(this, socket); // your existing handler
signaling.handle(io, socket, { userId, name }); // ← add this
});4. Generate tokens for your clients
Before a user can start or join a call, your backend generates a token and sends it to them. Do this from a REST route or another socket event:
// REST route example
app.post('/api/call/token', requireAuth, (req, res) => {
const { roomId, callId, callType } = req.body;
const token = signaling.generateToken({
userId: req.user._id,
roomId,
callId, // pass an existing callId when joining, omit/generate when initiating
callType, // 'audio' | 'video'
});
res.json({ token });
});Client Setup
1. Install
npm install @wavecall/clientOr via CDN (no bundler needed):
<script src="https://unpkg.com/@wavecall/client/dist/index.umd.js"></script>
<!-- WaveCall.WaveCallClient is now available globally -->2. Usage
import { WaveCallClient } from '@wavecall/client';
import { io } from 'socket.io-client';
const socket = io('https://your-server.com');
const client = new WaveCallClient({ socket });
// ─── Attach event handlers first ─────────────────────────────────────────
// Your own camera/mic stream → show in a <video> element
client.on('local_stream', ({ stream }) => {
document.getElementById('my-video').srcObject = stream;
});
// Remote participant's stream arrived
client.on('participant_joined', ({ userId, name, stream }) => {
const video = document.createElement('video');
video.srcObject = stream;
video.autoplay = true;
video.playsInline = true;
document.getElementById('participants').appendChild(video);
});
// A participant left
client.on('participant_left', ({ userId, name }) => {
// remove their video element from DOM
});
// Call ended for everyone
client.on('call_ended', ({ reason }) => {
console.log('Call ended:', reason); // 'all_left' | 'force_ended'
});
// Someone started a call in your room — show incoming call UI
client.on('incoming_call', async ({ callId, callType, initiator }) => {
if (confirm(`${initiator.name} is calling. Join?`)) {
const { token } = await fetch('/api/call/token', {
method: 'POST',
body: JSON.stringify({ callId, callType, roomId: currentRoomId }),
}).then(r => r.json());
await client.join({ callId, token });
}
});
// ─── Start a call ─────────────────────────────────────────────────────────
const { token } = await fetch('/api/call/token', {
method: 'POST',
body: JSON.stringify({ roomId, callType: 'video' }),
}).then(r => r.json());
await client.initiate({ roomId, callType: 'video', token });
// ─── Controls ─────────────────────────────────────────────────────────────
document.getElementById('mute-btn').onclick = () => client.toggleMute();
document.getElementById('video-btn').onclick = () => client.toggleVideo();
document.getElementById('screen-btn').onclick = () => client.toggleScreenShare();
document.getElementById('hand-btn').onclick = () => client.raiseHand();
document.getElementById('leave-btn').onclick = () => client.leave();
document.getElementById('end-btn').onclick = () => client.end(); // initiator onlyFull API Reference
WaveCallSignaling (server)
new WaveCallSignaling({ secret, expiresIn?, onCallStarted?, onCallEnded? })| Method | Description |
|---|---|
| generateToken(opts) | Generate a JWT token for a user |
| handle(io, socket, user) | Attach all signaling listeners to a socket |
WaveCallClient (browser)
new WaveCallClient({ socket, iceServers? })Methods
| Method | Returns | Description |
|---|---|---|
| initiate({ roomId, callType, token }) | Promise | Start a new call |
| join({ callId, token }) | Promise | Join an existing call |
| leave() | void | Leave gracefully |
| end() | void | End call for everyone (initiator only) |
| toggleMute() | boolean | Toggle mic, returns new isMuted state |
| toggleVideo() | boolean | Toggle camera, returns new isVideoOff state |
| toggleScreenShare() | Promise | Start/stop screen sharing |
| raiseHand() | void | Toggle raise/lower hand |
| muteParticipant(userId) | void | Force-mute someone (initiator only) |
| on(event, fn) | this | Add event listener (chainable) |
| off(event, fn) | void | Remove event listener |
Properties
| Property | Type | Description |
|---|---|---|
| localStream | MediaStream \| null | Your camera/mic stream |
| callId | string \| null | Active call ID |
| isInCall | boolean | Whether currently in a call |
Events
| Event | Payload | When |
|---|---|---|
| incoming_call | { callId, callType, initiator, roomId } | Someone started a call in your room |
| local_stream | { stream } | Your local camera/mic is ready |
| participant_joined | { userId, name, stream } | A remote user joined with their stream |
| participant_left | { userId, name } | A remote user disconnected |
| call_ended | { callId, reason } | Call ended for everyone |
| force_muted | { callId, mutedBy } | Admin muted your mic |
| hand_raised | { userId, name, isHandRaised } | Someone raised/lowered their hand |
| mute_changed | { isMuted } | Your own mute state changed |
| video_changed | { isVideoOff } | Your own video state changed |
| screen_share_started | {} | Screen share began |
| screen_share_stopped | {} | Screen share ended |
| error | { message, err } | Something went wrong |
Publishing to npm
# 1. Login to npm
npm login
# 2. Build both packages
cd packages/server && npm run build
cd ../client && npm run build
# 3. Publish
cd packages/server && npm publish --access public
cd ../client && npm publish --access publicFirst time? Make sure your npm account has 2FA enabled and the package names
@wavecall/serverand@wavecall/clientare available (or use your own npm org scope).
