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

whip-whep-client

v1.0.0

Published

A modern, TypeScript-first WHIP/WHEP client library for WebRTC streaming in the browser

Readme

whip-whep-client

A modern, TypeScript-first client library for the WHIP and WHEP WebRTC streaming protocols.

npm CI License: MIT npm downloads TypeScript


Overview

WHIP and WHEP are HTTP-based signalling protocols that make WebRTC streaming as simple as a single HTTP POST request. This library wraps the browser WebRTC APIs with a clean, event-driven interface so you can go from zero to streaming in a few lines of code.

Why this library?

Implementing WHIP or WHEP by hand is entirely possible — the protocol is just an HTTP POST followed by a few optional PATCH requests. In practice, however, the boilerplate grows quickly. The table below shows what you would need to handle yourself compared to using this library.

| Concern | Without this library | With this library | | ------------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------ | | RTCPeerConnection setup | Create, configure ICE servers, set bundle policy | Handled internally | | Adding tracks and transceivers | addTransceiver per kind, manage sendEncodings | Handled internally | | SDP offer/answer exchange | fetch POST, parse Location header, setRemoteDescription | Handled internally | | Trickle ICE via HTTP PATCH | Listen for icecandidate, batch and PATCH to resource URL | Handled internally | | Simulcast encodings | Manually add a=rid and a=simulcast SDP lines, set sendEncodings | simulcast: true | | Bitrate / Opus parameters | Munge SDP b=AS/b=TIAS, call sender.setParameters() after negotiation | Declarative options | | Authentication headers | Pass on every fetch call, refresh tokens manually | Static or async getHeaders | | Connection state tracking | Wire connectionstatechange, manage your own state machine | Typed events + state accessor | | Error context | Inspect raw Response.status, wrap DOMException for timeouts | Typed error classes with HTTP status | | Cleanup | DELETE resource URL, close RTCPeerConnection, stop tracks | client.stop() | | Auto-reconnect | Track attempt count, implement exponential backoff, re-run full signalling flow | autoReconnect: true or reconnect() | | Track replacement | Find the correct RTCRtpSender, call replaceTrack, update your stream refs | replaceTrack('video', newTrack) | | Stream statistics | Iterate RTCStatsReport, compute deltas, derive quality from loss + RTT | getStats() returns typed StreamStats | | ICE hang detection | Set up a manual timer, tear down the peer connection on expiry | iceConnectionTimeout option | | Logging | Sprinkle console.log calls, remove for production | logger option — pass any logger | | TypeScript types | Write your own interfaces or use loosely typed DOM APIs | Full types, zero any in public API |


Installation

npm install whip-whep-client

The package ships three output formats:

| Format | File | Use case | | -------- | ---------------------- | -------------------------------- | | ESM | dist/index.mjs | Bundlers (Vite, webpack, Rollup) | | CommonJS | dist/index.cjs | Node.js tooling | | IIFE | dist/index.global.js | <script> tag, CDN |

CDN (no build step)

<script src="https://unpkg.com/whip-whep-client/dist/index.global.js"></script>
<script>
    const { WHIPClient, WHEPClient } = WhipWhepClient;
</script>

Quick Start

Publish a stream (WHIP)

import { WHIPClient } from 'whip-whep-client';

const client = new WHIPClient({
    endpoint: 'https://ingest.example.com/whip/live',
    token: 'my-secret-token',
});

client.on('connected', () => console.log('Publishing'));
client.on('disconnected', () => console.log('Paused'));
client.on('failed', (err) => console.error('Error:', err));

const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
await client.publish(stream);

// When done:
await client.stop();

View a stream (WHEP)

import { WHEPClient } from 'whip-whep-client';

const client = new WHEPClient({
    endpoint: 'https://cdn.example.com/whep/stream/abc123',
    token: 'viewer-token',
});

client.on('stream', (stream) => {
    document.querySelector('video').srcObject = stream;
});

client.on('connected', () => console.log('Watching'));
client.on('failed', (err) => console.error('Error:', err));

await client.view();

// When done:
await client.stop();

WHIPClient

Publishes a MediaStream to a WHIP-compatible ingest server.

Constructor

new WHIPClient(options: WHIPClientOptions)

Options

| Option | Type | Default | Description | | ------------------------ | --------------------------------------------- | ---------------- | -------------------------------------------------------- | | endpoint | string | required | WHIP endpoint URL | | token | string | — | Sent as Authorization: Bearer <token> | | headers | Record<string, string> | — | Static custom headers on every request | | getHeaders | () => Record<string, string> \| Promise<...> | — | Dynamic headers resolved before each request | | iceServers | RTCIceServer[] | browser defaults | STUN / TURN servers | | iceTransportPolicy | 'all' \| 'relay' | 'all' | Set to 'relay' to force TURN | | iceCandidatePoolSize | number | 0 | Pre-gathered ICE candidate pool size | | timeout | number | 15000 | SDP exchange timeout in milliseconds | | iceConnectionTimeout | number | — | Max ms to wait for ICE 'connected' after SDP exchange | | autoReconnect | boolean \| AutoReconnectOptions | — | Reconnect automatically after a mid-session failure | | logger | Logger | — | Structured logger (e.g. console) | | simulcast | boolean | false | Enable simulcast (three quality layers) | | audioCodec | string | — | Preferred audio codec (e.g. 'opus') | | videoCodec | string | — | Preferred video codec (e.g. 'h264', 'vp8') | | audio | AudioEncodingOptions | — | Advanced audio encoding parameters | | video | VideoLayerOptions \| VideoLayerOptions[] | — | Advanced video encoding parameters | | maxBandwidth | number | — | Session bandwidth limit in kbps (b=AS in SDP) | | peerConnectionConfig | RTCConfiguration | — | Extra options merged into RTCPeerConnection config |

publish(stream, options?)

await client.publish(stream: MediaStream, options?: PublishOptions): Promise<void>

Starts publishing. Creates the RTCPeerConnection, adds media tracks, exchanges SDP with the server, and applies bitrate constraints.

PublishOptions

| Option | Type | Default | Description | | ----------- | --------- | ---------------- | -------------------------------- | | audio | boolean | true | Include the audio track | | video | boolean | true | Include the video track | | simulcast | boolean | from constructor | Override simulcast for this call |

stop()

await client.stop(): Promise<void>

Sends HTTP DELETE to release the server resource, closes the peer connection, and removes all event listeners. Safe to call multiple times.

reconnect()

await client.reconnect(): Promise<void>

Tears down the current peer connection and re-runs publish() with the stream from the last call. Useful for manually handling a 'failed' event. Requires that publish() was called at least once.

replaceTrack(kind, track)

await client.replaceTrack(kind: 'audio' | 'video', track: MediaStreamTrack): Promise<void>

Replaces the active sender track without renegotiation. The swap takes effect immediately via RTCRtpSender.replaceTrack(). Common use cases: switching from camera to screen share, muting via a silent track, or swapping microphone devices.

The stored stream used by reconnect() is updated automatically so future reconnects use the new track.

getStats()

const stats = await client.getStats(): Promise<StreamStats>

Returns a normalised snapshot of the current session statistics. Bitrate values are computed as a delta between the current and previous call, so calling getStats() periodically (e.g. every second) gives meaningful bitrate readings.

StreamStats

| Field | Type | Description | | ---------------- | ------------------------------------------- | --------------------------------------------- | | timestamp | number | Ms since epoch when the snapshot was taken | | audio | AudioStats \| null | Audio stats, null when no audio sender | | video | VideoStats \| null | Video stats, null when no video sender | | roundTripTime | number \| null | RTT in seconds from RTCP (null until first report) | | quality | 'excellent' \| 'good' \| 'fair' \| 'poor' | Derived from packet loss and RTT |

AudioStats / VideoStats additionally include bitrate (bps), packetsLost, packetsLostRate (0–1), jitter (s). VideoStats adds frameRate, width, and height.

Events

| Event | Arguments | Description | | -------------------------- | ------------------------------- | -------------------------------------------------- | | connected | — | ICE + DTLS fully established, media is flowing | | disconnected | — | Connection temporarily lost (may recover) | | failed | error: Error | Connection irrecoverably failed | | reconnecting | attempt: number, delayMs: number | Auto-reconnect attempt starting | | reconnected | — | Auto-reconnect successfully restored the session | | connectionstatechange | state: RTCPeerConnectionState | Raw connection state changes | | iceconnectionstatechange | state: RTCIceConnectionState | ICE connection state changes | | icegatheringstatechange | state: RTCIceGatheringState | ICE gathering state changes |


WHEPClient

Subscribes to a live stream from a WHEP-compatible server.

Constructor

new WHEPClient(options: WHEPClientOptions)

Options

Shares all base options with WHIPClient (endpoint, token, headers, getHeaders, iceServers, iceTransportPolicy, iceCandidatePoolSize, timeout, iceConnectionTimeout, autoReconnect, logger, peerConnectionConfig) plus:

| Option | Type | Default | Description | | -------------- | -------- | ------- | --------------------------------------------------------- | | audioCodec | string | — | Preferred inbound audio codec | | videoCodec | string | — | Preferred inbound video codec | | maxBandwidth | number | — | Bandwidth hint sent to server in kbps (b=AS in SDP) |

view(options?)

const stream = await client.view(options?: ViewOptions): Promise<MediaStream>

Returns a MediaStream that is populated with remote tracks as they arrive. The 'stream' event fires once all expected tracks are received.

ViewOptions

| Option | Type | Default | Description | | ------- | --------- | ------- | ------------- | | audio | boolean | true | Receive audio | | video | boolean | true | Receive video |

stop()

Stops all remote tracks, sends HTTP DELETE, and closes the peer connection. Safe to call multiple times.

reconnect()

await client.reconnect(): Promise<void>

Tears down the current peer connection and re-runs view() with the options from the last call. The new stream is delivered via the 'stream' event.

getStats()

const stats = await client.getStats(): Promise<StreamStats>

Same interface as WHIPClient.getStats(). Reads from inbound-rtp entries (receiver stats) and the active ICE candidate pair for RTT.

Events

Same as WHIPClient (connected, disconnected, failed, reconnecting, reconnected, connectionstatechange, iceconnectionstatechange, icegatheringstatechange), plus:

| Event | Arguments | Description | | -------- | --------------------- | ---------------------------------------------------- | | stream | stream: MediaStream | Remote stream ready to attach to a <video> element |


Advanced Usage

Custom Authentication Headers

Use headers for static custom schemes, or getHeaders when the header value must be recomputed per request (e.g. refreshable tokens, HMAC signatures).

// Static custom auth scheme
const client = new WHIPClient({
    endpoint: 'https://ingest.example.com/whip/live',
    headers: {
        'Authorization': 'Token abc123',
        'X-API-Key': 'my-key',
    },
});

// Dynamic – token is refreshed before every request
const client = new WHIPClient({
    endpoint: 'https://ingest.example.com/whip/live',
    getHeaders: async () => ({
        'Authorization': `Bearer ${await tokenStore.getValidToken()}`,
    }),
});

// HMAC request signature (timestamp-based)
const client = new WHEPClient({
    endpoint: 'https://cdn.example.com/whep/stream/abc',
    getHeaders: () => {
        const ts = Date.now().toString();
        return {
            'X-Timestamp': ts,
            'X-Signature': hmac(secret, ts),
        };
    },
});

Priority order (later entries override earlier ones):

  1. Built-in defaults (Content-Type, Authorization from token)
  2. Static headers
  3. Dynamic getHeaders() return value
  4. Per-request overrides (e.g. Content-Type: application/trickle-ice-sdpfrag for PATCH)

Advanced Audio Encoding

const client = new WHIPClient({
    endpoint: 'https://ingest.example.com/whip/live',
    audio: {
        maxBitrate: 128_000,       // 128 kbps
        dtx: true,                 // Discontinuous Transmission – saves bandwidth during silence
        stereo: true,              // Force stereo (default: mono)
        fec: true,                 // In-band FEC for packet loss recovery
        comfortNoise: false,       // Comfort noise generation
        contentHint: 'speech',     // Encoder hint: 'speech' | 'speech-recognition' | 'music'
    },
});

dtx, stereo, fec, and comfortNoise are written into the SDP offer as a=fmtp Opus parameters per RFC 7587. maxBitrate is applied via RTCRtpSender.setParameters() after negotiation.

Advanced Video Encoding (single layer)

const client = new WHIPClient({
    endpoint: 'https://ingest.example.com/whip/live',
    video: {
        maxBitrate: 2_500_000,      // 2.5 Mbps
        maxFramerate: 30,
        scaleResolutionDownBy: 1,   // Full resolution
        contentHint: 'motion',      // 'motion' | 'detail' | 'text'
    },
});

Simulcast

When simulcast: true, three quality layers are created by default. Override them to control each layer independently:

const client = new WHIPClient({
    endpoint: 'https://ingest.example.com/whip/live',
    simulcast: true,
    video: [
        { rid: 'high', maxBitrate: 2_500_000, scaleResolutionDownBy: 1 },
        { rid: 'mid',  maxBitrate: 1_000_000, scaleResolutionDownBy: 2 },
        { rid: 'low',  maxBitrate:   300_000, scaleResolutionDownBy: 4 },
    ],
});

Simulcast requires server-side support. Refer to your media server documentation (e.g. Janus, mediasoup, LiveKit) for configuration details.

Session Bandwidth

const client = new WHIPClient({
    endpoint: 'https://ingest.example.com/whip/live',
    maxBandwidth: 4_000,   // Adds b=AS:4000 and b=TIAS:4000000 to the SDP offer
});

b=AS is defined in RFC 4566 §5.8 and b=TIAS in RFC 3890. Actual enforcement depends on the server implementation.

Force TURN Relay

const client = new WHEPClient({
    endpoint: 'https://cdn.example.com/whep/stream/abc',
    iceServers: [{ urls: 'turn:turn.example.com', username: 'user', credential: 'pass' }],
    iceTransportPolicy: 'relay',   // Discard all non-relay candidates
});

Codec Preference

// Prefer H.264 and Opus on the sending side
const publisher = new WHIPClient({
    endpoint: '...',
    videoCodec: 'h264',
    audioCodec: 'opus',
});

// Prefer VP8 on the receiving side
const viewer = new WHEPClient({
    endpoint: '...',
    videoCodec: 'vp8',
});

Codec preference reorders the payload type list in the m= line of the SDP offer. The server is not obligated to honour it, but most implementations respect the order.

Screen Share (legacy)

WHIPClient.publish() accepts any MediaStream. For screen capture, prefer the getScreenStream utility which sets the correct contentHint automatically (see Screen Share in Advanced Usage). Raw getDisplayMedia also works:

const screen = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
await client.publish(screen, { audio: true, video: true });

Screen Share

Use getScreenStream to capture the screen with sensible defaults and the correct contentHint for the encoder:

import { WHIPClient, getScreenStream } from 'whip-whep-client';

const client = new WHIPClient({ endpoint: '...' });
const screen = await getScreenStream({ audio: true });

await client.publish(screen, { video: true, audio: true });

Override the defaults via videoConstraints:

const screen = await getScreenStream({
    audio: false,
    videoConstraints: { frameRate: { ideal: 15 }, width: { max: 1280 } },
});

Camera and Microphone

getUserStream is a convenience wrapper around getUserMedia that sets contentHint automatically:

import { getUserStream } from 'whip-whep-client';

const stream = await getUserStream({
    videoContentHint: 'motion',    // default — optimises for camera movement
    audioContentHint: 'speech',    // default — optimises Opus for voice
});
await client.publish(stream);

Replacing an Active Track

Switch from camera to screen share (or swap devices) mid-session without renegotiation:

import { getScreenStream } from 'whip-whep-client';

const screen = await getScreenStream();
await client.replaceTrack('video', screen.getVideoTracks()[0]);

// Switch back to camera:
const cam = await navigator.mediaDevices.getUserMedia({ video: true });
await client.replaceTrack('video', cam.getVideoTracks()[0]);

Auto-Reconnect

Pass autoReconnect: true to automatically retry when the connection fails after a successful session:

const client = new WHIPClient({
    endpoint: 'https://ingest.example.com/whip/live',
    token: 'my-token',
    autoReconnect: true,
});

client.on('reconnecting', (attempt, delayMs) => {
    console.log(`Reconnect attempt ${attempt} in ${delayMs}ms`);
});
client.on('reconnected', () => {
    console.log('Session restored');
});

const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
await client.publish(stream);

Fine-tune the retry policy with AutoReconnectOptions:

const client = new WHIPClient({
    endpoint: '...',
    autoReconnect: {
        maxAttempts: 10,          // default: 5
        initialDelayMs: 500,      // default: 1 000 ms — delay before 2nd attempt
        maxDelayMs: 60_000,       // default: 30 000 ms — cap on inter-attempt delay
        backoff: 'exponential',   // default — doubles each time; 'fixed' keeps initialDelayMs
    },
});

Manual reconnect is also available:

client.on('failed', async () => {
    await client.reconnect();
});

Auto-reconnect only fires when the connection was previously 'connected'. It does not retry signalling errors (e.g. a 401 from the server).

Connection Quality and Stats

Poll getStats() on an interval to monitor stream health:

import type { StreamStats } from 'whip-whep-client';

const timer = setInterval(async () => {
    const stats: StreamStats = await client.getStats();

    console.log('Quality:', stats.quality);          // 'excellent' | 'good' | 'fair' | 'poor'
    console.log('RTT:', stats.roundTripTime);         // seconds
    console.log('Video bitrate:', stats.video?.bitrate, 'bps');
    console.log('Packet loss:', stats.video?.packetsLostRate); // 0–1
}, 2_000);

// Clean up:
clearInterval(timer);

Quality thresholds:

| Quality | Packet-loss rate | RTT | | ----------- | --------------- | ----------- | | excellent | < 1 % | < 50 ms | | good | < 3 % | < 150 ms | | fair | < 8 % | < 300 ms | | poor | ≥ 8 % | ≥ 300 ms |

ICE Connection Timeout

Set iceConnectionTimeout to fail fast when ICE negotiation stalls:

const client = new WHIPClient({
    endpoint: '...',
    timeout: 10_000,              // SDP POST must complete within 10 s
    iceConnectionTimeout: 15_000, // ICE must reach 'connected' within 15 s
});

iceConnectionTimeout is independent of timeout. When the ICE deadline passes, a TimeoutError is thrown and the session is cleaned up.

Logging

Pass any object with debug, info, warn, and error methods:

// Development – log everything to the console
const client = new WHIPClient({
    endpoint: '...',
    logger: console,
});

// Production – structured logger (e.g. pino)
import pino from 'pino';
const client = new WHIPClient({
    endpoint: '...',
    logger: pino({ level: 'warn' }),
});

The logger receives messages for all significant internal events: HTTP requests, SDP exchange, ICE state changes, connection state transitions, and reconnect attempts.

Connection State Handling

const client = new WHIPClient({ endpoint: '...' });

client.on('connectionstatechange', (state) => {
    const actions = {
        connected:    () => showStatus('Live'),
        disconnected: () => showStatus('Reconnecting…'),
        failed:       () => { showStatus('Failed'); client.stop(); },
    };
    actions[state]?.();
});

Error Handling

All errors thrown by publish(), view(), and stop() extend WhipWhepError.

import { WHIPError, WHEPError, TimeoutError, InvalidStateError } from 'whip-whep-client';

try {
    await client.publish(stream);
} catch (err) {
    if (err instanceof TimeoutError) {
        console.error('SDP exchange took too long');
    } else if (err instanceof WHIPError && err.status === 401) {
        console.error('Unauthorized – check your token');
    } else if (err instanceof WHIPError && err.status === 503) {
        console.error('Server is at capacity');
    } else {
        throw err;
    }
}

| Class | When thrown | | ------------------- | ----------------------------------------------------------------------------------- | | WHIPError | WHIPClient.publish() — server rejected the offer or network error | | WHEPError | WHEPClient.view() — server rejected the offer or network error | | TimeoutError | SDP exchange exceeded options.timeout, or ICE exceeded iceConnectionTimeout | | InvalidStateError | Method called in wrong lifecycle state (e.g. publish() on a non-idle client) |

All error classes expose a status: number | undefined property containing the HTTP response status code.


SDP Utilities

Low-level SDP helpers are exported for advanced use cases (e.g. custom signalling layers, testing):

import { preferCodec, setBandwidth, addSimulcast, patchFmtp, listCodecs } from 'whip-whep-client';

// Prefer H.264 in the video section
const modifiedSdp = preferCodec(originalSdp, 'video', 'H264');

// Add a 3 Mbps bandwidth limit to the video section
const bwSdp = setBandwidth(originalSdp, 'video', 3_000);

// List all codec names in the audio section
const codecs = listCodecs(sdp, 'audio'); // ['opus', 'ISAC', ...]

// Patch Opus fmtp parameters
const opusSdp = patchFmtp(sdp, 'audio', 'opus', { usedtx: 1, stereo: 1 });

ICE Utilities

import { setupIceTrickle, waitForIceGathering } from 'whip-whep-client';

// Manual trickle ICE setup
const cleanup = setupIceTrickle(pc, {
    mode: 'end-of-candidates',   // or 'immediate'
    onCandidates: async (candidates) => {
        await fetch(resourceUrl, {
            method: 'PATCH',
            body: candidates.map((c) => `a=${c.candidate}`).join('\r\n'),
        });
    },
    onGatheringComplete: () => console.log('ICE gathering done'),
});

// Later:
cleanup();

TypeScript

The library is written in TypeScript and ships full type declarations. Generic type parameters are inferred automatically.

import type {
    WHIPClientOptions,
    WHEPClientOptions,
    AudioEncodingOptions,
    VideoLayerOptions,
    AutoReconnectOptions,
    Logger,
    StreamStats,
    ConnectionQuality,
    BaseClientEvents,
    WHEPClientEvents,
    ClientState,
} from 'whip-whep-client';

// Extend WHEPClientEvents to add custom events
interface MyPlayerEvents extends WHEPClientEvents {
    buffering: () => void;
}

Server Compatibility

The library works with any server that implements the WHIP or WHEP specification. Built-in presets provide recommended default options for the most widely used servers so you do not need to research per-server quirks manually.

Using presets

A preset is a plain options object that can be spread into the client constructor. You supply the endpoint; the preset fills in the codec, simulcast, and audio defaults that are known to work well with that server.

import { WHIPClient, livekit } from 'whip-whep-client';

const client = new WHIPClient({
    endpoint: 'https://my-project.livekit.cloud/rtc/whip',
    ...livekit.whip('my-access-token'),
});

Overriding a preset field is done through normal spread precedence:

const client = new WHIPClient({
    endpoint: 'https://...',
    ...livekit.whip(token),
    timeout: 20_000,       // overrides preset default
    simulcast: false,      // opt out of simulcast
});

LiveKit

LiveKit is an open-source WebRTC SFU with managed cloud and self-hosted options.

| | | | -------------- | ---------------------------------------------------------------------------------- | | WHIP endpoint | https://<project>.livekit.cloud/rtc/whip | | WHEP endpoint | https://<project>.livekit.cloud/rtc/whep | | Authentication | LiveKit Access Token (JWT) as Bearer token | | Docs | livekit.io/realtime/ingress/whip |

import { WHIPClient, WHEPClient, livekit } from 'whip-whep-client';

// Publish
const publisher = new WHIPClient({
    endpoint: 'https://my-project.livekit.cloud/rtc/whip',
    ...livekit.whip(accessToken),
});
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
await publisher.publish(stream);

// View
const viewer = new WHEPClient({
    endpoint: 'https://my-project.livekit.cloud/rtc/whep',
    ...livekit.whep(accessToken),
});
const remoteStream = await viewer.view();
videoEl.srcObject = remoteStream;

The preset enables simulcast and H.264 video by default, which are the recommended settings for LiveKit Cloud.


OvenMedia Engine

OvenMedia Engine (OME) is an open-source, real-time streaming server with native WHIP and WHEP support.

| | | | -------------- | ----------------------------------------------------------------------------------------------------- | | WHIP endpoint | http://<host>:3333/<app>/<stream> | | WHEP endpoint | http://<host>:3334/<app>/<stream> | | Authentication | Signed policy token (optional, configured per application) | | Docs | airensoft.gitbook.io/ovenmediaengine |

import { WHIPClient, WHEPClient, ovenmedia } from 'whip-whep-client';

// Publish (no auth)
const publisher = new WHIPClient({
    endpoint: 'http://localhost:3333/live/stream1',
    ...ovenmedia.whip(),
});

// Publish (with signed policy token)
const publisher = new WHIPClient({
    endpoint: 'http://localhost:3333/live/stream1',
    ...ovenmedia.whip('signed-policy-token'),
});

// View
const viewer = new WHEPClient({
    endpoint: 'http://localhost:3334/live/stream1',
    ...ovenmedia.whep(),
});

Cloudflare Stream

Cloudflare Stream is a managed video platform with WHIP ingest and WHEP egress support.

| | | | -------------- | ----------------------------------------------------------------------------------------------------- | | WHIP endpoint | https://customer-<uid>.cloudflarestream.com/<live-input-key>/webrtc/publish | | WHEP endpoint | https://customer-<uid>.cloudflarestream.com/<live-input-key>/webrtc/play | | Authentication | Embedded in the endpoint URL — no separate header required | | Docs | developers.cloudflare.com/stream/webrtc-beta |

The <live-input-key> is obtained when creating a Live Input via the Cloudflare Stream API or dashboard.

import { WHIPClient, WHEPClient, cloudflare } from 'whip-whep-client';

const liveInputKey = 'abc123...';
const uid = 'customer-xyz';

// Publish
const publisher = new WHIPClient({
    endpoint: `https://${uid}.cloudflarestream.com/${liveInputKey}/webrtc/publish`,
    ...cloudflare.whip(),
});

// View
const viewer = new WHEPClient({
    endpoint: `https://${uid}.cloudflarestream.com/${liveInputKey}/webrtc/play`,
    ...cloudflare.whep(),
});

Cloudflare Stream requires H.264 video. The preset enforces this and sets a 128 kbps audio limit that aligns with Cloudflare's recommended encoding settings.


Ant Media Server

Ant Media Server (AMS) is an open-source media server with Community and Enterprise editions, both supporting WHIP and WHEP.

| | | | -------------- | -------------------------------------------------------------------------------------------------- | | WHIP endpoint | http://<host>:5080/<app>/whip/<streamId> | | WHEP endpoint | http://<host>:5080/<app>/whep/<streamId> | | Authentication | JWT Bearer token (Enterprise, when authentication is enabled) | | Docs | antmedia.io/docs — WHIP |

import { WHIPClient, WHEPClient, antmedia } from 'whip-whep-client';

// Publish (Community, no auth)
const publisher = new WHIPClient({
    endpoint: 'http://localhost:5080/WebRTCAppEE/whip/stream1',
    ...antmedia.whip(),
});

// Publish (Enterprise, with JWT)
const publisher = new WHIPClient({
    endpoint: 'https://ams.example.com:5443/WebRTCAppEE/whip/stream1',
    ...antmedia.whip(jwtToken),
});

// View
const viewer = new WHEPClient({
    endpoint: 'http://localhost:5080/WebRTCAppEE/whep/stream1',
    ...antmedia.whep(),
});

Other servers

Any WHIP or WHEP-compliant server works without a preset. Pass the endpoint and any required authentication directly:

import { WHIPClient } from 'whip-whep-client';

const client = new WHIPClient({
    endpoint: 'https://media.example.com/whip/room1',
    token: 'bearer-token',
    videoCodec: 'h264',
});

If you have verified that a server works well with specific options, contributions of new presets are welcome — see CONTRIBUTING.md.


Protocol References


Browser Support

Requires a browser with support for the WebRTC 1.0 API (RTCPeerConnection, MediaStream). This covers all modern browsers (Chrome 56+, Firefox 44+, Safari 11+, Edge 79+).

No polyfills are included. The library does not support Node.js at runtime (only as a type-check / build-time dependency).


Contributing

See CONTRIBUTING.md for development setup, commit conventions, and release workflow.


License

MIT