whip-whep-client
v1.0.0
Published
A modern, TypeScript-first WHIP/WHEP client library for WebRTC streaming in the browser
Maintainers
Readme
whip-whep-client
A modern, TypeScript-first client library for the WHIP and WHEP WebRTC streaming protocols.
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.
- RFC 9725 — WebRTC-HTTP Ingestion Protocol (WHIP) — push a live video/audio stream to any compatible media server.
- draft-ietf-wish-whep — WebRTC-HTTP Egress Protocol (WHEP) — subscribe to a live stream from any compatible media server or CDN.
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-clientThe 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):
- Built-in defaults (
Content-Type,Authorizationfromtoken) - Static
headers - Dynamic
getHeaders()return value - Per-request overrides (e.g.
Content-Type: application/trickle-ice-sdpfragfor 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
- RFC 9725 — WebRTC-HTTP Ingestion Protocol (WHIP) — published March 2025, IETF Standards Track
- draft-ietf-wish-whep — WebRTC-HTTP Egress Protocol (WHEP) — IETF working draft
- RFC 8829 — JavaScript Session Establishment Protocol (JSEP) — SDP offer/answer model used by WebRTC
- RFC 4566 — Session Description Protocol (SDP) —
b=ASand other bandwidth attributes - RFC 3890 — A Transport Independent Bandwidth Modifier for SDP (TIAS) —
b=TIAS - RFC 7587 — RTP Payload Format for the Opus Speech and Audio Codec — Opus
a=fmtpparameters (DTX, FEC, stereo) - W3C WebRTC 1.0 —
RTCPeerConnection,RTCRtpSender.setParameters(),RTCRtpEncodingParameters
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.
