@moqtap/trace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/SPEC.md +228 -0
- package/dist/index.cjs +493 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +493 -0
- package/package.json +61 -0
- package/src/binary.ts +351 -0
- package/src/index.ts +33 -0
- package/src/json.ts +21 -0
- package/src/recorder.ts +247 -0
- package/src/types.ts +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 moqtap
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @moqtap/trace
|
|
2
|
+
|
|
3
|
+
Record, serialize, and analyze MoQT (Media over QUIC Transport) sessions using the `.moqtrace` binary format.
|
|
4
|
+
|
|
5
|
+
- Binary `.moqtrace` format with CBOR encoding (compact, streamable, cross-language)
|
|
6
|
+
- Configurable detail levels from control-only to full wire captures
|
|
7
|
+
- Session recorder that wraps `@moqtap/codec` session state machines
|
|
8
|
+
- Human-readable JSON export for debugging
|
|
9
|
+
- Zero-copy streaming writer for large traces
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @moqtap/trace @moqtap/codec
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`@moqtap/codec` is a peer dependency — install it alongside `@moqtap/trace`.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Recording a session
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createRecorder, writeMoqtrace } from '@moqtap/trace';
|
|
25
|
+
import { createDraft14SessionState } from '@moqtap/codec/draft14/session';
|
|
26
|
+
|
|
27
|
+
const recorder = createRecorder({
|
|
28
|
+
protocol: 'moq-transport-14',
|
|
29
|
+
perspective: 'client',
|
|
30
|
+
detail: 'control',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Wrap a session to auto-capture control messages and state transitions
|
|
34
|
+
const session = createDraft14SessionState({ codec: { draft: 'draft-ietf-moq-transport-14' }, role: 'client' });
|
|
35
|
+
const traced = recorder.wrapSession(session);
|
|
36
|
+
|
|
37
|
+
// Use `traced` instead of `session` — all send/receive calls are recorded
|
|
38
|
+
traced.send(clientSetupMessage);
|
|
39
|
+
traced.receive(serverSetupMessage);
|
|
40
|
+
|
|
41
|
+
// Manually record events the session layer doesn't see
|
|
42
|
+
recorder.recordStreamOpened(4n, 0, 0); // outgoing subgroup stream
|
|
43
|
+
recorder.annotate('connected', { relay: 'cdn.example.com' });
|
|
44
|
+
|
|
45
|
+
// Finalize and serialize
|
|
46
|
+
const trace = recorder.finalize();
|
|
47
|
+
const bytes = writeMoqtrace(trace); // → Uint8Array (.moqtrace binary)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Reading a trace file
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { readMoqtrace, readMoqtraceHeader } from '@moqtap/trace';
|
|
54
|
+
|
|
55
|
+
// Quick metadata peek (no event parsing)
|
|
56
|
+
const header = readMoqtraceHeader(bytes);
|
|
57
|
+
console.log(header.protocol); // "moq-transport-14"
|
|
58
|
+
console.log(header.perspective); // "client"
|
|
59
|
+
console.log(header.detail); // "control"
|
|
60
|
+
|
|
61
|
+
// Full parse
|
|
62
|
+
const trace = readMoqtrace(bytes);
|
|
63
|
+
for (const event of trace.events) {
|
|
64
|
+
console.log(event.type, event.timestamp);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Streaming writer (for large traces)
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { createMoqtraceWriter } from '@moqtap/trace';
|
|
72
|
+
|
|
73
|
+
const writer = createMoqtraceWriter(header);
|
|
74
|
+
outputStream.write(writer.preamble());
|
|
75
|
+
|
|
76
|
+
for (const event of events) {
|
|
77
|
+
outputStream.write(writer.writeEvent(event));
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Detail Levels
|
|
82
|
+
|
|
83
|
+
| Level | What's recorded |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `control` | Control messages only (setup, subscribe, publish, goaway) |
|
|
86
|
+
| `headers` | + data stream headers (subgroup/fetch, object metadata) |
|
|
87
|
+
| `headers+sizes` | + payload byte lengths |
|
|
88
|
+
| `headers+data` | + full payload bytes |
|
|
89
|
+
| `full` | + raw wire bytes (pre-decode) |
|
|
90
|
+
|
|
91
|
+
## JSON Export
|
|
92
|
+
|
|
93
|
+
For human-readable debugging (not lossless — bigint/Uint8Array become strings):
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { traceToJSON } from '@moqtap/trace';
|
|
97
|
+
|
|
98
|
+
const json = traceToJSON(trace);
|
|
99
|
+
console.log(json);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## File Format
|
|
103
|
+
|
|
104
|
+
See [SPEC.md](./SPEC.md) for the complete `.moqtrace` binary format specification.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
package/SPEC.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# `.moqtrace` File Format Specification
|
|
2
|
+
|
|
3
|
+
> **Version:** 1
|
|
4
|
+
> **Status:** Draft
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`.moqtrace` is a binary file format for recording MoQT (Media over QUIC Transport) sessions. It captures protocol-level events at configurable detail levels, from lightweight control-message-only traces to full wire-level recordings with payload bytes.
|
|
9
|
+
|
|
10
|
+
The format is designed to be:
|
|
11
|
+
|
|
12
|
+
- **Streamable** — events can be appended as they occur; no backpatching required
|
|
13
|
+
- **Compact** — CBOR encoding handles binary data natively (no base64 overhead)
|
|
14
|
+
- **Self-describing** — the header declares the detail level, protocol version, and recording context so readers know what to expect without scanning events
|
|
15
|
+
- **Cross-language** — readable by any language with a CBOR library (JS, Rust, Go, Python)
|
|
16
|
+
|
|
17
|
+
## File Layout
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Offset Length Content
|
|
21
|
+
------ ------ -------
|
|
22
|
+
0 8 Magic bytes: "MOQTRACE" (0x4d4f515452414345)
|
|
23
|
+
8 4 Format version (uint32 LE) — currently 1
|
|
24
|
+
12 4 Header length (uint32 LE) — byte length of the CBOR header blob
|
|
25
|
+
16 N Header (CBOR map) — session metadata and recording configuration
|
|
26
|
+
16+N ... Event stream (CBOR sequence) — concatenated CBOR items, one per event
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Magic Bytes
|
|
30
|
+
|
|
31
|
+
The 8-byte ASCII string `MOQTRACE` identifies the file format. Any tool can check these bytes before attempting to parse.
|
|
32
|
+
|
|
33
|
+
### Format Version
|
|
34
|
+
|
|
35
|
+
uint32 little-endian. Currently `1`. Readers MUST reject files with a version they don't support.
|
|
36
|
+
|
|
37
|
+
### Header Length
|
|
38
|
+
|
|
39
|
+
uint32 little-endian. The byte length of the CBOR-encoded header that immediately follows. This allows readers to extract metadata without scanning the event stream — useful for file browsers, search indexing, and quick filtering ("show me all draft-14 client traces").
|
|
40
|
+
|
|
41
|
+
## Header
|
|
42
|
+
|
|
43
|
+
A single CBOR map with the following keys:
|
|
44
|
+
|
|
45
|
+
| Key | CBOR Type | Required | Description |
|
|
46
|
+
| --------------- | ----------- | -------- | --------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `"protocol"` | text string | Yes | MoQT version identifier (e.g., `"draft-14"`, `"rfc9999"`) |
|
|
48
|
+
| `"perspective"` | text string | Yes | Recording viewpoint: `"client"`, `"server"`, or `"observer"` |
|
|
49
|
+
| `"detail"` | text string | Yes | Detail level (see below) |
|
|
50
|
+
| `"startTime"` | integer | Yes | Recording start time (Unix epoch milliseconds) |
|
|
51
|
+
| `"transport"` | text string | No | Transport type (e.g., `"webtransport"`, `"raw-quic"`) |
|
|
52
|
+
| `"endTime"` | integer | No | Recording end time (Unix epoch milliseconds). Written when trace is finalized. |
|
|
53
|
+
| `"source"` | text string | No | Software that produced the trace (e.g., `"moqtap-devtools/0.1.0"`, `"my-relay/2.3.1"`) |
|
|
54
|
+
| `"endpoint"` | text string | No | Remote peer URI (e.g., `"https://relay.example.com/moq"`) |
|
|
55
|
+
| `"sessionId"` | text string | No | Opaque identifier for correlating traces from the same session across multiple capture points |
|
|
56
|
+
| `"custom"` | map | No | User-defined metadata (arbitrary key-value pairs) |
|
|
57
|
+
|
|
58
|
+
### Protocol Identifier
|
|
59
|
+
|
|
60
|
+
The `"protocol"` field identifies the MoQT version without assuming lifecycle stage. Use the IETF document name minus the `draft-ietf-` prefix for drafts, and the RFC number for published standards:
|
|
61
|
+
|
|
62
|
+
- Draft phase: `"moq-transport-07"`, `"moq-transport-14"`
|
|
63
|
+
- RFC phase: `"moq-transport-rfc9999"`
|
|
64
|
+
|
|
65
|
+
### Perspective
|
|
66
|
+
|
|
67
|
+
The `"perspective"` field describes the vantage point of the recording:
|
|
68
|
+
|
|
69
|
+
- `"client"` — captured at the MoQT client (initiator of the QUIC connection)
|
|
70
|
+
- `"server"` — captured at the MoQT server or relay
|
|
71
|
+
- `"observer"` — passive capture (e.g., DevTools extension, network tap) that intercepts traffic without participating in the session
|
|
72
|
+
|
|
73
|
+
### Detail Levels
|
|
74
|
+
|
|
75
|
+
The `"detail"` field declares what was recorded. Each level is a strict superset of the one above it. Readers can handle any level — they just find more or fewer fields populated per event.
|
|
76
|
+
|
|
77
|
+
| Detail Level | What's Recorded | Typical Use Case |
|
|
78
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
|
|
79
|
+
| `"control"` | Control messages only (setup, subscribe, publish, goaway, etc.). No data stream events, no object payloads. | Lightweight protocol flow analysis. DevTools default. |
|
|
80
|
+
| `"headers"` | Control messages + data stream headers (subgroup/fetch/datagram headers, object metadata: group, object ID, priority, status). No payload bytes. | Delivery pattern analysis, timing. |
|
|
81
|
+
| `"headers+sizes"` | Everything in `"headers"` + payload byte lengths for each object. | Bandwidth analysis without storing media. |
|
|
82
|
+
| `"headers+data"` | Everything in `"headers"` + full payload bytes for each object. | Full session replay, debugging media corruption. |
|
|
83
|
+
| `"full"` | Everything above + raw wire bytes for every message (pre-decode). | Wire-level debugging, compliance testing. |
|
|
84
|
+
|
|
85
|
+
At any detail level, the recorder MAY be configured to **mask payloads** — replacing payload bytes with zeroes before writing. This preserves payload sizes for bandwidth analysis while stripping media content. The header SHOULD include `"payloadMasked": true` in the `"custom"` map when this is active.
|
|
86
|
+
|
|
87
|
+
## Event Stream
|
|
88
|
+
|
|
89
|
+
After the header, the remainder of the file is a sequence of concatenated CBOR items (one per event). This is a standard CBOR sequence (RFC 8742) — no array wrapper, no separators.
|
|
90
|
+
|
|
91
|
+
### Common Event Fields
|
|
92
|
+
|
|
93
|
+
Every event is a CBOR map with at minimum:
|
|
94
|
+
|
|
95
|
+
| Key | CBOR Type | Description |
|
|
96
|
+
| ----- | ---------------- | --------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| `"n"` | unsigned integer | Monotonically increasing sequence number (0-based). Disambiguates events with identical timestamps. |
|
|
98
|
+
| `"t"` | integer | Timestamp in microseconds since `startTime` in the header |
|
|
99
|
+
| `"e"` | integer | Event type (see table below) |
|
|
100
|
+
|
|
101
|
+
Short key names are used intentionally — traces can contain hundreds of thousands of events, and CBOR encodes short strings more compactly.
|
|
102
|
+
|
|
103
|
+
### Event Types
|
|
104
|
+
|
|
105
|
+
| `"e"` Value | Name | Description |
|
|
106
|
+
| ----------- | --------------- | -------------------------------------------------------- |
|
|
107
|
+
| `0` | Control message | A control-stream message was sent or received |
|
|
108
|
+
| `1` | Stream opened | A unidirectional or bidirectional QUIC stream was opened |
|
|
109
|
+
| `2` | Stream closed | A QUIC stream was closed |
|
|
110
|
+
| `3` | Object header | An object header was parsed from a data stream |
|
|
111
|
+
| `4` | Object payload | Object payload bytes were received/sent |
|
|
112
|
+
| `5` | State change | Session FSM phase transition |
|
|
113
|
+
| `6` | Error | Protocol error or transport error |
|
|
114
|
+
| `7` | Annotation | User-defined event (custom label + data) |
|
|
115
|
+
|
|
116
|
+
### Event-Specific Fields
|
|
117
|
+
|
|
118
|
+
#### Event 0: Control Message
|
|
119
|
+
|
|
120
|
+
| Key | Type | Detail Level | Description |
|
|
121
|
+
| ------- | ----------- | ------------ | ------------------------------------------------- |
|
|
122
|
+
| `"d"` | integer | `control`+ | Direction: `0` = sent (tx), `1` = received (rx) |
|
|
123
|
+
| `"mt"` | integer | `control`+ | Wire message type ID (e.g., `0x03` for SUBSCRIBE) |
|
|
124
|
+
| `"msg"` | map | `control`+ | Decoded message fields (draft-specific structure) |
|
|
125
|
+
| `"raw"` | byte string | `full` only | Raw wire bytes (including type and length prefix) |
|
|
126
|
+
|
|
127
|
+
#### Event 1: Stream Opened
|
|
128
|
+
|
|
129
|
+
| Key | Type | Detail Level | Description |
|
|
130
|
+
| ------- | ------- | ------------ | -------------------------------------------------------- |
|
|
131
|
+
| `"sid"` | integer | `headers`+ | QUIC stream ID |
|
|
132
|
+
| `"d"` | integer | `headers`+ | Direction: `0` = outgoing, `1` = incoming |
|
|
133
|
+
| `"st"` | integer | `headers`+ | Stream type: `0` = subgroup, `1` = datagram, `2` = fetch |
|
|
134
|
+
|
|
135
|
+
#### Event 2: Stream Closed
|
|
136
|
+
|
|
137
|
+
| Key | Type | Detail Level | Description |
|
|
138
|
+
| ------- | ------- | ------------ | ---------------------------- |
|
|
139
|
+
| `"sid"` | integer | `headers`+ | QUIC stream ID |
|
|
140
|
+
| `"ec"` | integer | `headers`+ | Error code (0 = clean close) |
|
|
141
|
+
|
|
142
|
+
#### Event 3: Object Header
|
|
143
|
+
|
|
144
|
+
Object header events and object payload events (event 4) for the same object share `(sid, g, o)` as a composite key. An object header event is always emitted before its corresponding payload event.
|
|
145
|
+
|
|
146
|
+
| Key | Type | Detail Level | Description |
|
|
147
|
+
| ------- | ------- | ------------ | -------------------------------------------------------------------------- |
|
|
148
|
+
| `"sid"` | integer | `headers`+ | Stream ID this object arrived on |
|
|
149
|
+
| `"g"` | integer | `headers`+ | Group ID |
|
|
150
|
+
| `"o"` | integer | `headers`+ | Object ID |
|
|
151
|
+
| `"pp"` | integer | `headers`+ | Publisher priority |
|
|
152
|
+
| `"os"` | integer | `headers`+ | Object status (0=normal, 1=end-of-group, 2=end-of-track, 3=does-not-exist) |
|
|
153
|
+
|
|
154
|
+
#### Event 4: Object Payload
|
|
155
|
+
|
|
156
|
+
| Key | Type | Detail Level | Description |
|
|
157
|
+
| ------- | ----------- | ---------------- | ----------------------------------- |
|
|
158
|
+
| `"sid"` | integer | `headers+sizes`+ | Stream ID |
|
|
159
|
+
| `"g"` | integer | `headers+sizes`+ | Group ID |
|
|
160
|
+
| `"o"` | integer | `headers+sizes`+ | Object ID |
|
|
161
|
+
| `"sz"` | integer | `headers+sizes`+ | Payload size in bytes |
|
|
162
|
+
| `"pl"` | byte string | `headers+data`+ | Payload bytes (or zeroed if masked) |
|
|
163
|
+
|
|
164
|
+
#### Event 5: State Change
|
|
165
|
+
|
|
166
|
+
| Key | Type | Detail Level | Description |
|
|
167
|
+
| -------- | ----------- | ------------ | ---------------------- |
|
|
168
|
+
| `"from"` | text string | `control`+ | Previous session phase |
|
|
169
|
+
| `"to"` | text string | `control`+ | New session phase |
|
|
170
|
+
|
|
171
|
+
#### Event 6: Error
|
|
172
|
+
|
|
173
|
+
| Key | Type | Detail Level | Description |
|
|
174
|
+
| ---------- | ----------- | ------------ | --------------------- |
|
|
175
|
+
| `"ec"` | integer | `control`+ | Error code |
|
|
176
|
+
| `"reason"` | text string | `control`+ | Human-readable reason |
|
|
177
|
+
|
|
178
|
+
#### Event 7: Annotation
|
|
179
|
+
|
|
180
|
+
| Key | Type | Detail Level | Description |
|
|
181
|
+
| --------- | ----------- | ------------ | --------------------------------- |
|
|
182
|
+
| `"label"` | text string | any | User-defined label |
|
|
183
|
+
| `"data"` | any | any | User-defined data (any CBOR type) |
|
|
184
|
+
|
|
185
|
+
## Reading a `.moqtrace` File
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
1. Read 8 bytes, verify magic == "MOQTRACE"
|
|
189
|
+
2. Read 4 bytes (uint32 LE) → format version. Reject if unsupported.
|
|
190
|
+
3. Read 4 bytes (uint32 LE) → header length N.
|
|
191
|
+
4. Read N bytes → CBOR-decode to get header map.
|
|
192
|
+
5. Read remaining bytes as a CBOR sequence:
|
|
193
|
+
- Repeatedly CBOR-decode one item at a time until EOF.
|
|
194
|
+
- Each item is one event (a CBOR map).
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Writing a `.moqtrace` File
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
1. Write magic bytes "MOQTRACE"
|
|
201
|
+
2. Write format version (uint32 LE, currently 1)
|
|
202
|
+
3. CBOR-encode the header map → get header bytes
|
|
203
|
+
4. Write header length (uint32 LE)
|
|
204
|
+
5. Write header bytes
|
|
205
|
+
6. For each event:
|
|
206
|
+
- CBOR-encode the event map
|
|
207
|
+
- Append to file
|
|
208
|
+
7. When finalizing:
|
|
209
|
+
- Optionally seek back and update header (e.g., set endTime)
|
|
210
|
+
- Or accept that endTime may be absent in crash-truncated files
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Versioning and Compatibility
|
|
214
|
+
|
|
215
|
+
- **Version 1** is defined by this document.
|
|
216
|
+
- Readers MUST check the format version before parsing.
|
|
217
|
+
- Unknown keys in header maps or event maps MUST be ignored (forward-compatible).
|
|
218
|
+
- New event types (`"e"` values) MAY be added in future versions; readers SHOULD skip unknown event types.
|
|
219
|
+
- The detail level in the header is informational — readers MUST handle missing fields gracefully regardless of declared level.
|
|
220
|
+
|
|
221
|
+
## Interoperability
|
|
222
|
+
|
|
223
|
+
The `.moqtrace` format is shared between the JavaScript (`@moqtap/trace`) and Rust (`moqtap-trace`) implementations. A file written by either is readable by both. CBOR libraries used:
|
|
224
|
+
|
|
225
|
+
- JavaScript: `cbor-x`
|
|
226
|
+
- Rust: `ciborium`
|
|
227
|
+
|
|
228
|
+
Both produce deterministic CBOR output (canonical CBOR is NOT required, but map keys SHOULD be sorted for consistency).
|