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

@playninja/voice-room

v0.2.0

Published

Tiny, transport-agnostic voice/audio rooms on top of the Cloudflare Realtime SFU. Bring your own signaling.

Downloads

287

Readme

@playninja/voice-room

Tiny, transport-agnostic voice/audio rooms on top of the Cloudflare Realtime SFU. Bring your own signaling — wire it to a game WebSocket, a Durable Object, anything.

  • One peer connection per client (SFU topology) — you push your mic once, the SFU forwards it to everyone.
  • No media servers to run. Cloudflare's SFU does the routing; TURN is built in and free alongside it.
  • Zero runtime dependencies. ~1 small file of client code.
  • Your secret stays server-side via a thin proxy helper.
  • Built-in mute, speaking detection / levels, and reconnection.

Audio-first (perfect for party games & social deduction). Video is a future addition — the SFU and API already support it.

Why "bring your own signaling"?

WebRTC needs a side-channel to announce "I'm here, here's my session/track id." Most apps already have one (a game socket). This library rides on yours via a 2-method Transport, so it drops into an existing realtime app without standing up anything new. It never relays SDP/ICE peer-to-peer — only tiny roster messages.

Install

npm install @playninja/voice-room

Quick start

1. Server — proxy the SFU (keeps your secret off the client)

import { handleRealtimeProxy } from '@playninja/voice-room/server';

// in your Worker fetch handler:
if (url.pathname.startsWith('/voice')) {
  return handleRealtimeProxy(request, '/voice', {
    appId: env.REALTIME_APP_ID,
    token: env.REALTIME_APP_SECRET, // wrangler secret put REALTIME_APP_SECRET
  });
}

2. Client — join the room

import { createVoiceRoom } from '@playninja/voice-room';

const voice = createVoiceRoom({
  apiBase: '/voice',          // your proxy route from step 1
  localId: myPlayerId,        // stable id
  transport: {                // wire to your existing socket
    send: (msg) => socket.send(JSON.stringify({ type: 'voice', msg })),
    onMessage: (handler) => {
      const fn = (e) => { const d = JSON.parse(e.data); if (d.type === 'voice') handler(d.msg); };
      socket.addEventListener('message', fn);
      return () => socket.removeEventListener('message', fn);
    },
  },
});

voice.on('peers', (peers) => renderSpeakingIndicators(peers)); // [{id, speaking, level, muted, connected}]
voice.on('status', (s) => console.log('voice', s));

await voice.join();          // call from a click (mic permission + autoplay)
voice.setMuted(true);
await voice.leave();

Your server must also relay the voice messages to the other players in the room (the same fan-out you already do for chat/game state). The SDK only produces/consumes the payloads.

API

| | | |---|---| | createVoiceRoom(options) | Create a room (doesn't join). | | voice.join() | Mic permission → push track → pull peers. Call from a user gesture. | | voice.setMuted(bool) | Enable/disable your mic (no renegotiation). | | voice.resumeAudio() | Call from a gesture if needsgesture fired (autoplay blocked). | | voice.leave() | Tear everything down. | | voice.peers / voice.status / voice.muted | Current state. |

Events: status, peers, level ({id, level, speaking}, frequent), error, needsgesture.

Options: apiBase, transport, localId, iceServers?, audio?, speakingThreshold?.

Run the demo (your spike)

Two tabs on one machine hearing each other through real Cloudflare media:

  1. Create a Realtime app in the Cloudflare dashboard → copy the App ID + App Secret.
  2. npm install
  3. npm run build:demo
  4. Create demo/.dev.vars:
    REALTIME_APP_ID = "your-app-id"
    REALTIME_APP_SECRET = "your-app-secret"
  5. wrangler dev --config demo/wrangler.jsonc → open http://localhost:8787 in two tabs, same room code, Join in both.

(The demo uses BroadcastChannel for signaling, which works across tabs in one browser. Real cross-device use needs a real transport like your game socket.)

How it works

 each client                         Cloudflare Realtime SFU
 ┌──────────┐   push mic (1 track)   ┌──────────────────────┐
 │ browser  │ ─────────────────────▶ │   forwards to all     │
 │  (1 PC)  │ ◀───────────────────── │   pullers             │
 └──────────┘   pull N peer tracks   └──────────────────────┘
      ▲  roster (sessionId + trackName) over YOUR transport
      ▼
 ┌──────────┐
 │ your app │  (game WebSocket / Durable Object)
 └──────────┘

License

MIT