Skip to content

@moqtap/inspect

Make decoded MoQT control messages readable.

npm | GitHub

@moqtap/inspect takes the control messages @moqtap/codec decodes and turns them into what a person debugging a session wants to read:

  • Readable fields: setup versions as draft-NN, byte strings as text where they are printable, integers past 2^53 in full, and the implementation each end names in its setup message.
  • A track registry: track aliases joined to the namespace and name the control plane gave them.
  • Error names: an error or status code by its name in the session’s draft.
  • Request pairing: each request matched to its response, with the round-trip time.
  • Problems: error responses, requests left unanswered, blocked request IDs, GOAWAY and undecodable bytes, in time order and in one line each.
  • Rollup delivery: each track’s delivery figures from the rollups in a @moqtap/collector trace.

Supports drafts 07 through 22. Works in Node.js, Bun, Deno and browsers. Every result is plain data, so it survives postMessage and JSON.stringify.

Terminal window
npm install @moqtap/inspect @moqtap/codec

@moqtap/codec (0.13.2 or later) is a peer dependency: the error code names come from its registries.

import {
displayFields,
errorName,
pairRequests,
problems,
problemTitle,
setupImplementation,
TrackRegistry,
trackDisplayName,
type TrackFields,
} from '@moqtap/inspect'
displayFields('client_setup', msg)
// { supported_versions: [{ $shown: 'version', text: 'draft-14', raw: '0xff00000e' }], ... }
setupImplementation('client_setup', msg)
// { side: 'client', implementation: 'moxygen 0.4' }
const tracks = new Map<string, TrackFields>()
const registry = new TrackRegistry(tracks, (fields) => ({ ...fields }))
registry.apply('subscribe_ok', msg, { direction: 'rx' })
const track = registry.byAlias('3')
if (track) trackDisplayName(track) // 'live/ch4 / video/1080p'
errorName(14, 'SUBSCRIBE_ERROR', 4n) // 'TRACK_DOES_NOT_EXIST'
errorName(22, 'REQUEST_ERROR', 0x32n) // null: not a draft-22 code
const events = [
{ name: 'SUBSCRIBE', fields: msg, direction: 'tx', streamId: 4, atMs: 0 },
{ name: 'REQUEST_ERROR', fields: err, direction: 'rx', streamId: 4, atMs: 212 },
]
pairRequests(events, { draft: 22 }) // [{ request, response, rttMs: 212 }]
const found = problems(events, { draft: 22, pendingMs: 5000 })
problemTitle(found[0], { draft: 22, pendingMs: 5000 })
// 'REQUEST_ERROR DOES_NOT_EXIST (0x10) "no such track"'

displayFields(name, fields) returns a message’s fields made readable. A value that needs both a shown form and the recorded one becomes a Displayed record, { $shown, text, raw }:

ValueShown as
A setup versiondraft-14, with the raw 0xff00000e beside it
Bytes that are printable ASCIItext
Other bytesa hex preview
An integer past 2^53full decimal

setupImplementation(name, fields) reads the moqt_implementation a CLIENT_SETUP, SERVER_SETUP or SETUP message advertises, and says which end sent it.

TrackRegistry keeps a session’s tracks up to date from its control messages. You pass it a Map it updates in place, and a function that builds your stored record, so you can keep your own fields beside the track’s:

const registry = new TrackRegistry(tracks, (fields) => ({ ...fields, firstSeen: Date.now() }))
for (const m of controlMessages) {
registry.apply(m.name, m.fields, { direction: m.direction, streamId: m.streamId })
}

apply() takes the message name in either case and returns the track it affected, or null. It follows each draft’s join: the alias in SUBSCRIBE (drafts 07–11), in SUBSCRIBE_OK or PUBLISH (12 and later), and the request stream for responses that carry no request ID (17 and later). Pass streamId for draft-17 sessions and later.

errorName(draft, message, code) returns a code’s name in that draft’s registry, or null. It never borrows a name from another draft: a code the draft does not define answers null. Pass session as the message for the code a session closed with, and stream_reset for a data stream’s reset code.

codeLabel(code, name, draft) formats the result for display: TRACK_DOES_NOT_EXIST (0x4), or 0x4, unknown for draft-14.

messageAliases(name, draft?) returns the same message’s names in other drafts, such as ANNOUNCE and PUBLISH_NAMESPACE, or SUBSCRIBE_DONE and PUBLISH_DONE.

pairRequests and problems take control events in the order they were seen:

{ name, fields, direction, streamId, atMs, connection }

and { kind: 'undecodable' } for bytes that did not decode. Fields may be raw from the codec or from displayFields.

A response is matched by request ID, by the namespace it repeats (ANNOUNCE and TRACK_STATUS_REQUEST through draft-10), or by the stream it came back on (draft-17 and later), and only against requests that went the other way on the same connection.

problems(events, { draft, pendingMs }) reports:

kindWhen
errorA *_ERROR or REQUEST_ERROR, with the code’s name and the request it answers
pendingA request without a response pendingMs after it was sent
requests-blockedREQUESTS_BLOCKED or SUBSCRIBES_BLOCKED, repeats counted in count
request-limit-reachedA request used the last ID the peer’s MAX_REQUEST_ID allowed
goawayGOAWAY, with the URI it names
undecodableBytes that did not decode

problemTitle and problemDetail put a problem into words. They take the same draft and pendingMs, plus perspective (client or server, the end that recorded the session) to say which end sent a GOAWAY, and trackOf(event) to name a request’s track. problemSeverity(kind) is error for an error response or undecodable bytes, and warning for the rest.

rollupDelivery(annotations) takes a collector trace’s annotations in trace order, { segment, atMs, label, data, connection }, and sums each track’s collector.rollup figures per connection and direction:

  • objects, bytes, group ID gaps and skipped objects;
  • stalls, and the longest one;
  • cadence and arrival percentiles;
  • on the send side, time blocked and bytes acknowledged.

It returns tracks, one entry per track, and intervals, one per track per rollup interval.

Durations are buckets on the collector’s ladder: bucket i holds [2^i, 2^(i+1)) ms. quantileBucket(counts, q) finds the bucket holding a quantile of a histogram, and highestBucket(counts) the highest one with a count.

ExportWhat it does
displayFields(name, fields)A message’s fields made readable, setup versions as drafts
displayValue(value)One value made readable
displayBytes(bytes)Text when printable ASCII, otherwise a hex preview
setupImplementation(name, fields)The moqt_implementation a setup message names, and which end
setupVersionLabel(version)0xff00000e as draft-14
draftOfProtocol(protocol)The draft a negotiated protocol (moqt-16) or label names
TrackRegistryJoins track aliases to namespace and name from control messages
trackDisplayName(track)namespace / name for a registered track
errorName(draft, message, code)A code’s name in that draft’s registry, or null
messageAliases(name, draft?)The same message’s names in other drafts
pairRequests(events, { draft })Each request with its response and round-trip time
problems(events, { pendingMs, draft })Errors, pending requests, blocked request IDs, GOAWAY and undecodable bytes, in time order
rollupDelivery(annotations)Each track’s delivery and rollup intervals from a collector trace’s collector.rollup annotations
quantileBucket(counts, q)The duration-ladder bucket holding a quantile of a histogram
highestBucket(counts)The highest duration-ladder bucket with a count
problemTitle(problem, options)A problem in one line
problemDetail(problem, options)The request an error answers, or a pending request’s track; null when there is nothing more
problemSeverity(kind)error for an error response or undecodable bytes, warning for the rest
codeLabel(code, name, draft)TRACK_DOES_NOT_EXIST (0x4), or 0x4, unknown for draft-14
requestIdOf(name, fields)The request ID a message carries, in decimal

MIT.