para:rtp
import rtp from "para:rtp";A small RTP toolkit — pack a payload into an RFC 3550 packet, parse one off the wire, and reorder by sequence number with a configurable depth. Built to sit under para:audio’s Opus encoder for a WebRTC-style send/receive path.
pack(opts)
Section titled “pack(opts)”Returns a Uint8Array containing the RTP header + payload.
const packet = rtp.pack({ payloadType: 111, // 7 bits sequence: 1234, // 16 bits timestamp: 48000, // 32 bits ssrc: 0xDEADBEEF, // 32 bits payload: opusFrame, // Uint8Array marker: false, // bool, default false});CSRCs and extensions aren’t supported — the header is fixed at the 12-byte minimum. Open an issue if you need them.
parse(bytes)
Section titled “parse(bytes)”Parses a single RTP packet. Returns { payloadType, sequence, timestamp, ssrc, payload, marker }. Throws if the version field isn’t 2 or the length is too short.
const { payloadType, sequence, timestamp, payload } = rtp.parse(packet);JitterBuffer
Section titled “JitterBuffer”Reorders incoming packets by sequence number. Useful when the network delivers out-of-order or duplicates.
const buf = new rtp.JitterBuffer({ depth: 8 });
// ingest as packets arrivebuf.push(packet);buf.push(packet2);
// drain in orderfor (const ordered of buf.drain()) { decoder.decode(ordered.payload);}| Option | Default | Description |
|---|---|---|
depth | 8 | Max reorder window. Packets older than tail - depth are dropped. |
wrapAware | true | Handles 16-bit sequence-number wrap. |
drain() yields packets in sequence order until it would have to wait for a missing one. Subsequent push calls + drain cycles continue from where it stopped.
Reactive signals
Section titled “Reactive signals”Three para:signals Signals on the buffer instance — wire them into a UI without polling.
| Signal | Type | When it changes |
|---|---|---|
jb.pendingSignal | number | Number of packets buffered, waiting on the next-expected slot. Updates synchronously on every push / pop. |
jb.lossCountSignal | number | Cumulative count of packets declared lost since construction. Increments when a missing slot ages out past maxLag. |
jb.lossRateSignal | number | Lifetime loss ratio: lossCount / (lossCount + delivered). Recomputes on every delivered or lost transition. |
import { effect } from "para:signals";
effect(() => { if (jb.lossRateSignal.get() > 0.05) console.warn("packet loss > 5%");});session.connected and session.jitterMs from PLAN-module-signals.md need a future Session abstraction (RTP / RTCP correlation, source-arrival timestamp differencing) — neither exists in para:rtp v1. When a Session class lands, those signals will join the surface there.
A full audio pipeline
Section titled “A full audio pipeline”Combined with para:audio:
import audio from "para:audio";import rtp from "para:rtp";
await using mic = await audio.capture({ sampleRate: 48000, channels: 1 });const enc = new audio.OpusEncoder({ sampleRate: 48000, channels: 1, application: "voip" });const den = new audio.Denoiser();const agc = new audio.Gain({ targetLevel: 0.1 });
let sequence = 0, timestamp = 0;const ssrc = (Math.random() * 0xFFFFFFFF) | 0;
for await (const frame of mic.frames()) { den.process(frame.samples); agc.process(frame.samples); const opus = enc.encode(frame.samples);
const packet = rtp.pack({ payloadType: 111, sequence: sequence++, timestamp, ssrc, payload: opus, }); // send `packet` over your transport (UDP, WebRTC, etc.). console.log("packet bytes:", packet.byteLength);
timestamp += frame.samples.length; // advance by sample count}Limits
Section titled “Limits”- Single-stream — no SDES / RTCP companion.
- The jitter buffer is sequence-only. Packet-loss concealment, FEC, and rate-adaptive depth are all on the encoder/decoder side (
para:audio.OpusDecoderhandles in-band PLC). - IPv4 / IPv6 wire transport itself is up to the caller —
para:rtpproduces / consumes bytes, not sockets.