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

@originchats/voice

v0.1.0

Published

Framework-agnostic voice/video (WebRTC mesh) client for OriginChats servers.

Readme

@originchats/voice

Framework-agnostic voice & video (screen share + camera) client for OriginChats servers. It runs a WebRTC mesh over PeerJS and takes care of the fiddly parts — connection setup, glare avoidance, retries/reconnection, per-user volume, speaking detection, and encoder quality tuning — behind a small, UI-agnostic API.

It has no framework dependency. You give it a signaling transport, some live settings getters, and a few optional hooks; it gives you a subscribe() stream of immutable state snapshots you can render however you like.

npm install @originchats/voice peerjs

Why it exists

Every OriginChats client had to re-implement the same tricky WebRTC dance and kept hitting the same problems: black video tiles, one-way audio, blurry screen shares, and joins that silently failed for some users. This package centralizes that logic with those bugs fixed (see What it fixes).

Quick start

import { VoiceClient } from "@originchats/voice";

const voice = new VoiceClient({
  // 1. How presence is announced to your OriginChats server.
  signaling: {
    join: (channel, peerId) => ws.send({ cmd: "voice_join", channel, peer_id: peerId }),
    leave: () => ws.send({ cmd: "voice_leave" }),
    setMuted: (muted) => ws.send({ cmd: muted ? "voice_mute" : "voice_unmute" }),
  },

  // 2. Live settings (read on demand, so changes apply mid-call).
  settings: {
    getMicThreshold: () => 30, // speaking-detection sensitivity, 0-100
    getVideoHeight: () => 1080,
    getVideoFps: () => 30,
    getStartMuted: () => false,
  },

  // 3. (Recommended) your own STUN/TURN for reliability behind strict NATs.
  iceServers: [
    { urls: "stun:stun.l.google.com:19302" },
    { urls: "turn:turn.example.com:3478", username: "user", credential: "pass" },
  ],
});

// Render state however you like.
const unsubscribe = voice.subscribe((state) => {
  render(state.participants, state.screenStreams, state.cameraStreams /* … */);
});

// Feed your server's voice broadcasts back in.
ws.on("voice_join", (m) => voice.onJoined(m.channel, m.participants));
ws.on("voice_user_joined", (m) => voice.onUserJoined(m.channel, m.user));
ws.on("voice_user_left", (m) => voice.onUserLeft(m.channel, m.username));
ws.on("voice_user_updated", (m) => voice.onUserUpdated(m.channel, m.user));

// Drive it.
await voice.join("general", { username: "alice", serverId: ws.url });
voice.toggleMute();
voice.toggleCamera();
voice.toggleScreenShare();
voice.leave();

How it works

  • Signaling (who is in the channel) goes over your OriginChats WebSocket — the package never talks to your server directly, it just calls your signaling adapter and expects you to route the matching broadcasts back into onJoined / onUserJoined / onUserLeft / onUserUpdated.
  • Media negotiation (SDP/ICE) goes through a PeerJS broker, keyed by the peer_id each client announces on join. By default this is the public PeerJS cloud, which is rate-limited — self-host one for production.
  • Each pair of participants shares one bidirectional audio connection; each active screen share / camera is a separate one-way connection.

What it fixes

Compared to a naive PeerJS mesh, this package addresses the common failure modes:

| Symptom | Cause | Fix | | --- | --- | --- | | "I can't hear someone / they can't hear me" | The call initiator dropped the answer stream, making audio one-way. | Both ends play the peer's stream; audio is fully duplex over one connection. | | Black video tiles | Both peers called each other at once (glare), leaving connections stuck. | Deterministic initiator (lower peer_id calls; the other waits with a fallback). | | Blurry / low-quality screen share | Default contentHint: "motion" trades resolution for framerate. | Screen tracks are tagged "detail" with maintain-resolution degradation. | | Low quality in general | No sender bitrate ceiling; browser under-allocates on a mesh. | Per-kind maxBitrate derived from resolution, applied once media flows. | | "It just doesn't connect" for some users | Single free TURN with expiring credentials. | TURN/STUN are configurable; supply your own. | | Silent failures | No connection state surfaced. | Each participant carries a connection state (connecting/reconnecting/failed). |

Self-hosting the broker & TURN

The public PeerJS cloud and the bundled free TURN are best-effort fallbacks only. For anything real, run your own:

new VoiceClient({
  // …
  peerBroker: { host: "peer.example.com", port: 443, path: "/", secure: true },
  iceServers: [
    { urls: "stun:stun.example.com:3478" },
    { urls: "turn:turn.example.com:3478", username: "u", credential: "p" },
  ],
});
  • Broker: peerjs-server (npx peerjs --port 9000).
  • TURN: coturn is the standard choice. TURN is what makes calls work behind symmetric NATs and restrictive firewalls — the usual reason "it works for me but not for them."

API

new VoiceClient(options)

| Option | Required | Description | | --- | --- | --- | | signaling | ✅ | { join, leave, setMuted } — announce presence to your server. | | settings | ✅ | Live getters: getMicThreshold, getVideoHeight, getVideoFps, optional getStartMuted, getDefaultMicId, getDefaultCamId. | | iceServers | – | STUN/TURN list. Defaults to public STUN + shared TURN. | | peerBroker | – | PeerJS broker { host, port, path, key, secure }. Defaults to the PeerJS cloud. | | quality | – | maxBitrate / contentHint / degradationPreference overrides. | | hooks | – | Optional side-effects — see below. | | storage | – | { getItem, setItem } for per-user prefs. Defaults to localStorage, then memory. | | logger | – | (level, ...args) => void. Defaults to console.warn. |

Hooks (all optional): shouldBlockJoin(channel), onBlockedJoin(channel), onMutedChange(muted), onCameraOnChange(on), onSelectedMicChange(id), onSelectedCamChange(id), onViewVisibility(action).

Methods

  • subscribe(cb) => unsubscribe, getState()
  • join(channel, { username?, serverId? }), leave()
  • onJoined, onUserJoined, onUserLeft, onUserUpdated, setMyUsername
  • toggleMute(), toggleDeafen(), toggleCamera(), toggleScreenShare()
  • switchMicrophone(id), switchCamera(id), flipCamera(), restartCamera(), restartScreenShare()
  • setUserVolume/getUserVolume, setUserMuted/getUserMuted, setStreamVolume/getStreamVolume, setStreamMuted/getStreamMuted
  • getAudioInputDevices(), getVideoInputDevices(), setDefaultMicId(id), setDefaultCamId(id)
  • Getters: currentChannel, serverId, isMuted, isDeafened, isSpeaking, callStartTime, cameraFacing, selectedMicId, selectedCamId, isInChannel(), getMyPeerId()

State snapshot

subscribe emits an immutable VoiceSnapshot: participants (each with speaking, muted, locallyMuted, localVolume, connection), screenStreams / cameraStreams (keyed by peer_id), the local streams, mute/deafen flags, and the persisted per-user volume/mute maps.

License

MIT