npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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/server

2. Add to .env

WAVECALL_SECRET=any_long_random_string_here

3. 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/client

Or 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 only

Full 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 public

First time? Make sure your npm account has 2FA enabled and the package names @wavecall/server and @wavecall/client are available (or use your own npm org scope).