Skip to content

@moqtap/codec

Encode, decode, and validate MoQT messages in JavaScript/TypeScript.

npm | GitHub

@moqtap/codec is a complete MoQT protocol library with two layers:

  1. Codec — stateless encode/decode of all control messages, data streams, and VarInts
  2. Session FSM — protocol state machine that validates message ordering, tracks subscriptions and fetches, and emits side effects

Supports draft-07 and draft-14 of MoQ Transport. Zero runtime dependencies. Works in Node.js, Bun, and browsers.

Terminal window
npm install @moqtap/codec
import { createCodec } from '@moqtap/codec';
const codec = createCodec({ draft: '14' });
// Encode a message to wire bytes
const bytes = codec.encodeMessage({
type: 'subscribe',
requestId: 1n,
trackNamespace: ['live'],
trackName: 'video',
subscriberPriority: 128,
groupOrder: 'original',
forward: 0n,
filterType: 'latest_group',
parameters: new Map(),
});
// Decode wire bytes back to a typed message
const result = codec.decodeMessage(bytes);
if (result.ok) {
console.log(result.value.type); // 'subscribe'
}

Each draft has its own entry point with the full API:

// Draft-14 (recommended)
import {
createDraft14Codec,
encodeMessage,
decodeMessage,
createStreamDecoder,
} from '@moqtap/codec/draft14';
// Draft-07
import {
createDraft07Codec,
encodeMessage,
decodeMessage,
} from '@moqtap/codec/draft7';
const codec = createDraft14Codec();
// Single message
const bytes = codec.encodeMessage(message);
const result = codec.decodeMessage(bytes);
// Streaming decoder for incremental parsing
const decoder = codec.createStreamDecoder();
// TransformStream<Uint8Array, Draft14Message>
// Subgroup stream
codec.encodeSubgroupStream(stream);
codec.decodeSubgroupStream(bytes);
codec.createSubgroupStreamDecoder();
// Datagrams
codec.encodeDatagram(obj);
codec.decodeDatagram(bytes);
// Fetch streams
codec.encodeFetchStream(stream);
codec.decodeFetchStream(bytes);
codec.createFetchStreamDecoder();
import { encodeVarInt, decodeVarInt } from '@moqtap/codec/draft14';
const bytes = encodeVarInt(16384n); // 4-byte encoding
const result = decodeVarInt(bytes); // { ok: true, value: 16384n }

The session FSM validates protocol message ordering and tracks the lifecycle of subscriptions, publishes, and fetches. It does not perform I/O — you feed it messages and it tells you whether they’re legal.

import { createSessionState } from '@moqtap/codec/session';
const session = createSessionState({ draft: 'moq-transport-14', role: 'client' });
MethodDescription
send(message)Validate and apply an outgoing message. Returns TransitionResult.
receive(message)Validate and apply an incoming message. Returns TransitionResult.
validateOutgoing(message)Check if a message is legal to send without changing state.
reset()Return to idle phase, clear all tracked state.
PropertyTypeDescription
phaseSessionPhaseCurrent session phase
role'client' or 'server'The local endpoint’s role
subscriptionsReadonlyMapSubscription states keyed by ID
publishesReadonlyMapPublish states keyed by request ID (draft-14)
fetchesReadonlyMapFetch states keyed by request ID (draft-14)
legalOutgoingReadonlySetMessage types valid to send now
legalIncomingReadonlySetMessage types valid to receive now

Every send() and receive() call returns a result:

// Success
{ ok: true, phase: 'ready', sideEffects: [...] }
// Protocol violation
{ ok: false, violation: { code: 'ROLE_VIOLATION', message: '...', currentPhase: 'ready', offendingMessage: 'subscribe_ok' } }

Violation codes: MESSAGE_BEFORE_SETUP, UNEXPECTED_MESSAGE, DUPLICATE_SUBSCRIBE_ID, DUPLICATE_REQUEST_ID, UNKNOWN_SUBSCRIBE_ID, UNKNOWN_REQUEST_ID, ROLE_VIOLATION, STATE_VIOLATION, SETUP_VIOLATION.

idle → setup → ready → draining
PhaseEntered whenLegal messages
idleInitial stateclient_setup (client)
setupclient_setup sent/receivedserver_setup (server)
readyserver_setup receivedAll control messages
draininggoaway sent/receivedExisting subscriptions only

The FSM emits side effects to notify your application of state changes:

const result = session.receive(message);
if (result.ok) {
for (const effect of result.sideEffects) {
switch (effect.type) {
case 'session-ready':
// Setup complete, can send control messages
break;
case 'session-draining':
// GoAway received, effect.goAwayUri has the new URI
break;
case 'subscription-activated':
// SUBSCRIBE_OK received, effect.subscribeId
break;
case 'subscription-ended':
// Subscription closed, effect.reason
break;
case 'fetch-activated': // draft-14
case 'fetch-ended': // draft-14
case 'publish-activated': // draft-14
case 'publish-ended': // draft-14
break;
}
}
}
import { createSessionState } from '@moqtap/codec/draft14/session';
const session = createSessionState({ role: 'client' });
// Setup
session.send({ type: 'client_setup', versions: [0xff00000en], parameters: new Map() });
session.receive({ type: 'server_setup', selectedVersion: 0xff00000en, parameters: new Map() });
// session.phase === 'ready'
// Subscribe
const sub = session.send({
type: 'subscribe',
requestId: 1n,
trackNamespace: ['chat'],
trackName: 'messages',
subscriberPriority: 128,
groupOrder: 'original',
forward: 0n,
filterType: 'latest_object',
parameters: new Map(),
});
// sub.ok === true, subscription is now 'pending'
session.receive({
type: 'subscribe_ok',
requestId: 1n,
expires: 0n,
groupOrder: 'original',
contentExists: true,
parameters: new Map(),
});
// subscription is now 'active', sideEffects includes 'subscription-activated'
// Check what we can send
session.legalOutgoing; // Set { 'subscribe', 'unsubscribe', 'fetch', ... }

Failed decodes throw or return error results with codes:

CodeMeaning
UNEXPECTED_ENDTruncated input
INVALID_VARINTMalformed variable-length integer
UNKNOWN_MESSAGE_TYPEUnrecognized message type ID
INVALID_PARAMETERMalformed parameter encoding
CONSTRAINT_VIOLATIONValue outside legal range