Skip to content

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.

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.

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);

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 arrive
buf.push(packet);
buf.push(packet2);
// drain in order
for (const ordered of buf.drain()) {
decoder.decode(ordered.payload);
}
OptionDefaultDescription
depth8Max reorder window. Packets older than tail - depth are dropped.
wrapAwaretrueHandles 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.

Three para:signals Signals on the buffer instance — wire them into a UI without polling.

SignalTypeWhen it changes
jb.pendingSignalnumberNumber of packets buffered, waiting on the next-expected slot. Updates synchronously on every push / pop.
jb.lossCountSignalnumberCumulative count of packets declared lost since construction. Increments when a missing slot ages out past maxLag.
jb.lossRateSignalnumberLifetime 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.

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
}
  • 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.OpusDecoder handles in-band PLC).
  • IPv4 / IPv6 wire transport itself is up to the caller — para:rtp produces / consumes bytes, not sockets.