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

@observertc/samples-protobuf-codec

v3.3.0

Published

Protobuf delta codec for ObserveRTC ClientSamples — encodes each sample as the difference from the previous one

Readme

@observertc/samples-protobuf-codec

License

Protobuf codec for ObserveRTC ClientSamples.

It is a delta codec: each message on the wire carries the difference between one sample and the one before it. That is where nearly all of the size saving comes from — a WebRTC stats sample is mostly identifiers, codec parameters and slow-moving counters that repeat verbatim every tick.

This package replaces @observertc/samples-encoder and @observertc/samples-decoder, which were the same codec split across two published artefacts.

Protobuf only. The name says so on purpose. A sample stream can be diffed against other wire formats, and those belong in their own packages — the field mapping, the presence rules and the version compatibility story are all different.

Install

npm install @observertc/samples-protobuf-codec

Ships both ESM and CommonJS builds with full type declarations. Node ≥ 20, and any modern browser — nothing here touches Buffer or other Node built-ins.

Usage

Encoding, on the client

import { ClientSampleEncoder } from '@observertc/samples-protobuf-codec';

const encoder = new ClientSampleEncoder({ clientId });

setInterval(async () => {
  const sample = await collectClientSample();
  websocket.send(encoder.encode(sample));      // Uint8Array
}, 1000);

For a text-only transport there is encodeToBase64(sample).

Decoding, on the server

import { ClientSampleDecoder } from '@observertc/samples-protobuf-codec';

const decoder = new ClientSampleDecoder();

websocket.on('message', (bytes) => {
  const sample = decoder.decode(bytes);        // a complete ClientSample
  pipeline.push(sample);
});

One decoder per connection, and it must see that connection's messages in order. See Streams below.

Both halves at once

import { createClientSampleCodec } from '@observertc/samples-protobuf-codec';

const codec = createClientSampleCodec({ clientId, identifiers: { clientId: 'uuid' } });
const sample = codec.decoder.decode(codec.encoder.encode(input));

Building both from one options object is the point: the encoder and the decoder have to agree about identifier packing, and the failure mode when they do not is mojibake rather than an error.

Streams are ordered and stateful

Message 5 says "jitter is now 12" and says nothing at all about the forty fields that did not move. A decoder that has not seen messages 1–4 therefore cannot reconstruct sample 5 — and this package tells you so, with a STREAM_DESYNC error, rather than handing back an object that is quietly missing its trackIdentifier.

Practical consequences:

  • One decoder per encoder. They are a pair.
  • The transport must be ordered and lossless. A WebSocket or an HTTP/2 stream is fine. A datagram transport is not, unless you add your own ordering.
  • reset() is a keyframe. Call it on both halves at the same point in the stream — after a reconnect, on a schedule, or whenever a new receiver needs to be able to join. The next message is a full snapshot readable by a decoder that has seen nothing.
encoder.reset();
const keyframe = encoder.encode(sample);
new ClientSampleDecoder().decode(keyframe);   // works

clientId is the one exception: it rides on every message, so a receiver can always attribute a message without keeping routing state of its own.

A field that stops being reported keeps its last value

A delta message says what changed. It has no way to say "this field is gone", so the decoder returns the running forward-fill of the stream: if sample 4 carries jitter and sample 5 does not, the decoded sample 5 still has sample 4's jitter.

That is almost always what you want from getStats() output, where a field missing from one tick means the browser did not report it rather than that it became meaningless. It does mean the decoder's output is "everything known about this client so far", not "exactly the object the encoder was handed".

Collection membership works the other way round: the newest sample defines it. An RTP stream, codec or candidate pair that stops appearing is understood to have left the call, and its state is dropped on both sides — so if it comes back, it is re-sent in full.

Identifiers: utf8 or uuid

Identifier fields (callId, clientId, peerConnectionId, trackId / trackIdentifier) travel as bytes. If yours really are UUIDs, packing them into their 16 significant bytes halves them:

const options = {
  clientId,
  identifiers: {
    callId: 'uuid',
    clientId: 'uuid',
    peerConnectionId: 'uuid',
    trackId: 'uuid',
  },
} satisfies EncoderOptions;

Default is utf8 for all of them, because ObserveRTC does not require callers to use UUIDs. A value configured as uuid that is not a UUID is an INVALID_VALUE error at encode time, not a silent fallback.

Errors

Everything this package throws is a ProtobufCodecError with a code and a context.path pointing at the offending value:

| code | Meaning | | ----------------- | -------------------------------------------------------------------- | | MALFORMED_INPUT | The bytes are not a ClientSample protobuf message. | | STREAM_DESYNC | The decoder has not seen enough of the stream to rebuild this sample. | | INVALID_VALUE | A value could not be converted between its plain and its wire form. | | INVALID_OPTION | The codec was configured with something it cannot honour. |

const result = decoder.tryDecode(bytes);
if (!result.ok) {
  metrics.increment('codec.decode_failed', { code: result.error.code });
  return;
}

The package writes nothing to console on its own. Pass a logger (anything with debug/info/warn/error) if you want to hear about recoverable oddities.

API

| Export | What it is | | --------------------------------- | --------------------------------------------------------------------- | | ClientSampleEncoder | encode, encodeToBase64, encodeToMessage, reset. | | ClientSampleDecoder | decode, decodeBase64, decodeFromMessage, tryDecode, reset. | | createClientSampleCodec | Both halves from one options object. | | ProtobufCodecError | Every failure, with code and context. | | ClientSample and friends | The plain sample types, re-exported. | | protobuf | The generated protobuf bindings, namespaced. | | schemaVersion, PROTO_PACKAGE | What this build speaks. |

encodeToMessage / decodeFromMessage work in protobuf messages rather than bytes, for when the sample is going straight into another protobuf structure — or when you want to look at what a delta actually contains.

How it works

The codec keeps one small state machine per live thing in the call — the client, each peer connection, each RTP stream, each ICE candidate pair — holding the last value seen for every field. Encoding walks that tree and writes down only what moved. Decoding walks the same tree and lays the arriving values back over what it already had.

Two consequences worth knowing:

  • Collections are keyed, not positional. Peer connections match on peerConnectionId, RTP streams on ssrc, everything else on id. The key is repeated on every message so entries stay matchable; an entry that stops appearing is understood to have left the call.
  • The field mapping is read from the protobuf descriptor at runtime. Which fields exist, which are 64-bit, which are bytes, which nest — all of it comes off the descriptor rather than from hand-written per-field code. A field added upstream is carried by the codec without a line of new code.

Wire compatibility

The .proto is the all-optional variant of ClientSample, where explicit presence is what carries "this field changed". Field numbers are derived from the Avro schema and are part of the contract.

The test suite pins the actual bytes for two recorded streams (test/fixtures/*.golden.json). A change that alters them is a wire break and needs a schema version bump — a round-trip test would not notice, because encode and decode move together.

Generated code

src/generated/samples.ts and src/generated/protobuf.ts are produced by the schema generator in this repository and must not be edited by hand:

npm run generate:codec        # from the repository root

Everything else in src/ is hand-written and schema-agnostic.

Development

npm install
npm run typecheck
npm test
npm run test:update-goldens   # after a deliberate wire change
npm run build

License

Apache-2.0