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

@oddin-gg/havik-webrtc-player

v0.2.1

Published

Portable, dependency-free browser WebRTC client for havik-webrtc — HavikPlayer (watch/WHEP signaling + getStats telemetry), HavikMatchList (self-refreshing live match list), and HavikGateway.

Readme

@oddin-gg/havik-webrtc-player

Dependency-free browser client for Havik WebRTC — Oddin's sub-second live esports streaming API. It negotiates the WebRTC session with the Havik gateway, drops the live H.264 + Opus stream into your <video> element, and hands you normalized playback telemetry. No framework, no build step, and auth stays entirely in your code.

import { HavikPlayer } from '@oddin-gg/havik-webrtc-player';

const player = new HavikPlayer({
  gatewayUrl: 'https://webrtc-dev.oddin-video.gg',
  matchId: 'od:match:<uuid>',
  videoEl: document.querySelector('video'),
});
await player.connect();

Three exports:

| Export | What it does | | ------ | ------------ | | HavikPlayer | One viewer session: signaling handshake, playback, stats, end-of-stream handling. | | HavikMatchList | Self-refreshing "what's live right now" list for building a match picker. | | HavikGateway | Single config point (gateway URL + authed fetch) that spawns both, plus token minting and a liveness probe. |

Install

npm install @oddin-gg/havik-webrtc-player

ESM only, zero runtime dependencies, hand-written TypeScript declarations bundled (src/index.d.ts). No-build setups can copy src/index.js in directly — it is a single self-contained module.

How it works

The player drives the gateway's server-offer watch flow — the gateway sends the SDP offer, the browser answers (the reverse of WHEP). One POST /watch/{matchId} mints a session, the player applies the offer, posts its answer, trickles ICE, and media flows; disconnect() tears the session down. Capacity, task routing, TURN relay, and codec choices are all server-side concerns — the browser never configures them.

The gateway HTTP API is the stable, binding contract; this library is a convenience layer over it. Integrating from scratch, or need the full endpoint semantics, error model, and rate limits? Read the Havik WebRTC public API reference.

Authentication

Two independent surfaces, both kept out of the library:

  • Watching (HavikPlayer) — production gateways require a short-lived watch token per match per viewer, minted by your backend (it owns the entitlement decision) and passed via token or getToken. On dev the gateway is open and no token is needed.

    new HavikPlayer({
      // …
      getToken: async () => fetchTokenFromYourBackend(matchId), // fresh on every connect()
    });

    getToken takes precedence over the static token; return null for open deployments; a throw fails the connect().

  • Listing & minting (HavikMatchList, HavikGateway.mintWatchToken) — these call operator/tenant-tier endpoints. You supply an authedFetch that attaches whatever credential your host app holds (e.g. a Cognito Bearer token); the library never sees or stores it.

HavikPlayer

const player = new HavikPlayer({
  gatewayUrl: 'https://webrtc-dev.oddin-video.gg',
  matchId: 'od:match:<uuid>',
  videoEl: document.querySelector('video'),
  onState: (state, detail) => updateUi(state, detail),
  onStats: (s) => console.log(`${s.width}x${s.height} @${s.fps}fps ${s.videoKbps}kbps rtt=${s.rttMs}ms`),
  onError: (err) => console.warn('signaling error', err.status, err.message),
});

await player.connect();
// …
await player.disconnect(); // idempotent

Instances are reusable: connect() fully resets per-session state, so connect → disconnect → connect and reconnect-after-failure all work on the same instance. From an unload handler, use player.disconnect({ keepalive: true }) so the teardown request survives the page closing.

States

onState(state, detail?) walks this machine:

| State | Meaning | Detail | | ----- | ------- | ------ | | idle | Constructed, nothing in flight. | | | connecting | Handshake or ICE in progress (also re-emitted on a transient ICE drop). | note on re-emits | | connected | Media is flowing. | ttffMs once the first frame paints | | ended | Terminal. The server announced the match is over. | reason: 'match_ended' | | failed | The session failed; recovery is the host's call. | error message | | closed | Torn down by disconnect(). | |

End of stream

The server announces teardowns on a per-session control DataChannel just before closing the peer — the player handles the protocol for you:

  • Match overonState('ended', { reason: 'match_ended' }). Terminal: show your end-of-stream UI and don't reconnect. No onError fires (nothing failed) and the instance stays reusable.
  • Non-terminal server closes (server_shutdown during a rolling deploy, stream_unavailable, stopped, or a reason this build doesn't know) surface as failed with session closed by server (…) — run your normal reconnect logic; the match may still be live.
  • The notice is best-effort (a crashed task sends nothing), so classify unexplained failures with HavikGateway.isMatchLive(): false → the stream is over, true → reconnect, null → unknown (never treat as ended).

Failure messages are phase-aware: ICE could not establish only when the session never connected, transport failed mid-session after it had.

Telemetry

onStats delivers a normalized snapshot from RTCPeerConnection.getStats() every second (tune with statsIntervalMs): resolution, fps, video/audio bitrate, packets lost, jitter, RTT, codecs, time-to-first-frame, bytes received, frames decoded. See PlayerStats in the typings for the exact shape.

Errors & retries

Retryable signaling failures (429, 503 on POST /watch / POST /answer) are retried automatically, honoring Retry-AftermaxRetries: 2 (default) means up to 3 attempts. Signaling errors reach onError as HavikPlayerError, carrying status, retryable, and the parsed retryAfter seconds.

Options

| Option | Type | Default | Description | | ------ | ---- | ------- | ----------- | | gatewayUrl | string | — | Gateway base URL (trailing slash optional). | | matchId | string | — | Match URN, e.g. od:match:<uuid>. | | videoEl | HTMLVideoElement | — | Target element; the player attaches the remote stream. | | token | string \| null | null | Static Bearer watch token. | | getToken | () => string \| null \| Promise<…> | — | Token provider, called fresh on every connect(). Wins over token. | | iceServers | RTCIceServer[] | Google STUN | STUN for the browser's reflexive candidate. No client TURN needed — the server side provides relay. | | statsIntervalMs | number | 1000 | onStats cadence. | | maxRetries | number | 2 | Extra attempts on retryable handshake calls. | | onState / onStats / onLog / onError | callbacks | — | See above; onLog streams leveled wire-protocol logs for debugging. |

HavikMatchList

A match picker without the plumbing: it owns the poll / diff / backoff loop and calls onUpdate only when the list actually changes.

import { HavikMatchList } from '@oddin-gg/havik-webrtc-player';

const list = new HavikMatchList({
  gatewayUrl: 'https://webrtc-dev.oddin-video.gg',
  authedFetch: (url, init) => fetch(url, withBearer(init)),
  intervalMs: 5000,
  onUpdate: (matches) => renderPicker(matches),
  onError: (err, status) => status === 403 && promptSignIn(),
});
list.start();   // one immediate load, then every intervalMs
// list.stop(); list.refresh({ force: true });

Each MatchInfo row carries match_id (the URN you hand to HavikPlayer), viewers, and — when the platform knows them — display metadata: match_name, tournament_name, sport. Render those when present and fall back to the URN; they are display-only, so never key logic on them:

const label = m.match_name
  ? [m.match_name, m.tournament_name, m.sport].filter(Boolean).join(' · ')
  : m.match_id;

HavikGateway

Set the gateway URL and authed fetch once, then spawn players and lists bound to it:

import { HavikGateway } from '@oddin-gg/havik-webrtc-player';

const gw = new HavikGateway({
  gatewayUrl: 'https://webrtc-dev.oddin-video.gg',
  authedFetch: (url, init) => fetch(url, withBearer(init)),
});

const list = gw.matchList({ onUpdate: renderPicker });
const player = gw.player({ matchId, videoEl });

await gw.mintWatchToken(matchId); // token, or null when minting is disabled
await gw.isMatchLive(matchId);   // true | false | null — see "End of stream"

Browser support

Any modern browser with RTCPeerConnection and native H.264 + Opus decoding — current Chrome, Edge, Firefox, Safari, and their Android/iOS equivalents. No MSE, no EME, no plugins.

Reference app

runstone is a plain-JS app built on this exact client (CI keeps the two byte-identical): sign-in, match picker, player, stats grid, and end-of-stream handling — a working example of everything above.

License

ISC © Oddin.gg