moqtail 0.7.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/README.md ADDED
@@ -0,0 +1,514 @@
1
+ # MOQtail TypeScript Client Library
2
+
3
+ > ⚠️ **Work in Progress**: This library is under active development and the API is subject to change. Please use with caution in production environments.
4
+
5
+ MOQT (Media over QUIC Transport) is a protocol for media delivery over QUIC connections, enabling efficient streaming of live and on-demand content. The MOQtail client library provides a TypeScript implementation that supports both publisher and subscriber roles in the MOQT ecosystem.
6
+
7
+ ## Overview
8
+
9
+ The `MOQtailClient` serves as the main entry point for interacting with MoQ relays and other peers. A client can act as:
10
+
11
+ - **Original Publisher**: Creates and announces tracks, making content available to subscribers
12
+ - **End Subscriber**: Discovers and consumes content from publishers via track subscriptions
13
+
14
+ ## Publisher
15
+
16
+ As a publisher, the MOQtail client allows you to create, manage, and distribute content through tracks. The library handles protocol-level details while giving you full control over content creation and packaging.
17
+
18
+ ### Track Management
19
+
20
+ Publishers can add or remove tracks using the `addOrUpdateTrack()` and `removeTrack()` methods:
21
+
22
+ ```typescript
23
+ const client = await MOQtailClient.new(clientSetup, webTransport)
24
+
25
+ // Add a new track
26
+ client.addOrUpdateTrack(myTrack)
27
+
28
+ // Remove an existing track
29
+ client.removeTrack(myTrack)
30
+ ```
31
+
32
+ ### Track Structure
33
+
34
+ Each track is defined by the `Track` interface, which consists of:
35
+
36
+ - **`fullTrackName`**: Unique identifier for the track (namespace + track name)
37
+ - **`trackAlias`**: Numeric alias used for efficient wire representation
38
+ - **`forwardingPreference`**: How objects should be delivered (Datagram or Subgroup)
39
+ - **`contentSource`**: The source of content for this track
40
+
41
+ ### Content Sources
42
+
43
+ The `ContentSource` interface is the heart of the publisher model, providing two distinct patterns for content delivery:
44
+
45
+ #### Live Content (Streaming)
46
+
47
+ For real-time content like live video streams, use `LiveContentSource`:
48
+
49
+ - Content flows through a `ReadableStream<MoqtObject>`
50
+ - Subscribers receive content via **Subscribe** operations
51
+ - Suitable for continuously generated content
52
+
53
+ #### Static Content (On-Demand)
54
+
55
+ For archived or pre-generated content, use `StaticContentSource`:
56
+
57
+ - Content is stored in an `ObjectCache` for random access
58
+ - Subscribers retrieve specific ranges via **Fetch** operations
59
+ - Ideal for video-on-demand, file transfers, or cached content
60
+
61
+ #### Hybrid Content
62
+
63
+ For tracks that support both patterns, use `HybridContentSource`:
64
+
65
+ - Combines live streaming with historical data access
66
+ - New objects are added to cache while also flowing to live subscribers
67
+
68
+ ### Object Packaging
69
+
70
+ All content is packaged as `MoqtObject` instances, which represent the atomic units of data in MoQ:
71
+
72
+ - **Location**: Identified by `groupId` and `objectId` (e.g., video frames within GOPs)
73
+ - **Payload**: The actual media data or content
74
+ - **Metadata**: Publisher priority, forwarding preferences, and extension headers
75
+ - **Status**: Normal data, end-of-group markers, or error conditions
76
+
77
+ ### Object Caching
78
+
79
+ The `ObjectCache` interface provides two simple implementations for static content:
80
+
81
+ - **`MemoryObjectCache`**: Unlimited in-memory storage with binary search indexing
82
+ - **`RingBufferObjectCache`**: Fixed-size cache with automatic eviction of oldest objects
83
+
84
+ ### Publisher Workflow
85
+
86
+ 1. **Create Content**: Generate or prepare your media content
87
+ 2. **Package as Objects**: Wrap content in `MoqtObject` instances with appropriate metadata
88
+ 3. **Choose Content Source**: Select `LiveContentSource`, `StaticContentSource`, or `HybridContentSource`
89
+ 4. **Define Track**: Create a `Track` with your content source and metadata
90
+ 5. **Add to Client**: Register the track with `addOrUpdateTrack()`
91
+ 6. **Publish Namespace**: Use `publishNamespace()` to make the track discoverable by subscribers
92
+ 7. **Manage Lifecycle**: The library handles incoming subscribe/fetch requests and data delivery
93
+
94
+ ### Example
95
+
96
+ ```typescript
97
+ // Create a live video track
98
+ const videoTrack: Track = {
99
+ fullTrackName: FullTrackName.tryNew('live/conference', 'video'),
100
+ trackAlias: 1n,
101
+ forwardingPreference: ObjectForwardingPreference.Subgroup,
102
+ contentSource: new LiveContentSource(videoStream),
103
+ }
104
+
105
+ // Create a static file track
106
+ const fileCache = new MemoryObjectCache()
107
+ // ... populate cache with file chunks ...
108
+ const fileTrack: Track = {
109
+ fullTrackName: FullTrackName.tryNew('files/documents', 'presentation.pdf'),
110
+ trackAlias: 2n,
111
+ forwardingPreference: ObjectForwardingPreference.Datagram,
112
+ contentSource: new StaticContentSource(fileCache),
113
+ }
114
+
115
+ // Register tracks and announce
116
+ client.addOrUpdateTrack(videoTrack)
117
+ client.addOrUpdateTrack(fileTrack)
118
+
119
+ await client.publishNamespace(new PublishNamespace(client.nextClientRequestId, Tuple.tryNew(['live', 'conference'])))
120
+ ```
121
+
122
+ The library automatically manages active requests, handles protocol negotiation, and ensures efficient data delivery based on subscriber demands and network conditions.
123
+
124
+ ## Subscriber
125
+
126
+ As a subscriber, the MOQtail client enables you to discover, request, and consume content from publishers. The library provides two main mechanisms for content retrieval: `subscribe()` for live streaming content and `fetch()` for on-demand content access.
127
+
128
+ ### Live Content Subscription
129
+
130
+ For real-time streaming content, use `subscribe()` which returns either a `ReadableStream<MoqtObject>` or a `SubscribeError`:
131
+
132
+ #### Subscribe Implementation
133
+
134
+ Subscribe operations are designed for live streaming and can be delivered through multiple transport mechanisms:
135
+
136
+ - **Datagrams**: For low-latency delivery where occasional packet loss is acceptable
137
+ - **Multiple Streams**: Each group (GOP) can be delivered in a separate stream for better prioritization
138
+ - **Stream Cancellation**: The library implements intelligent stream cancellation on both publisher and subscriber sides:
139
+ - **Publisher Side**: Automatically cancels streams for older groups when bandwidth is limited
140
+ - **Subscriber Side**: Cancels streams for groups that are no longer needed due to latency constraints
141
+
142
+ This approach ensures that subscribers always receive the most recent content with minimal latency, automatically dropping outdated frames during network congestion.
143
+
144
+ ```typescript
145
+ const subscribe = new Subscribe(
146
+ client.nextClientRequestId,
147
+ trackAlias, // Numeric alias for the track
148
+ fullTrackName, // Full track name
149
+ subscriberId, // Your subscriber ID
150
+ startGroup, // Starting group ID (or null for latest)
151
+ startObject, // Starting object ID (or null for latest)
152
+ endGroup, // Ending group ID (or null for ongoing)
153
+ endObject, // Ending object ID (or null for group end)
154
+ authInfo, // Authorization information
155
+ )
156
+
157
+ const result = await client.subscribe(subscribe)
158
+
159
+ if (result instanceof SubscribeError) {
160
+ console.error(`Subscription failed: ${result.reasonPhrase}`)
161
+ // Handle error based on error code
162
+ switch (result.errorCode) {
163
+ case SubscribeErrorCode.InvalidRange:
164
+ // Adjust range and retry
165
+ break
166
+ default:
167
+ console.error(`Unknown error: ${result.reasonPhrase}`)
168
+ }
169
+ } else {
170
+ // Success - result is ReadableStream<MoqtObject>
171
+ const objectStream = result
172
+ const reader = objectStream.getReader()
173
+
174
+ try {
175
+ while (true) {
176
+ const { done, value: object } = await reader.read()
177
+ if (done) break
178
+
179
+ // Process each object
180
+ console.log(`Received object ${object.objectId} from group ${object.groupId}`)
181
+ processObject(object)
182
+ }
183
+ } finally {
184
+ reader.releaseLock()
185
+ }
186
+ }
187
+ ```
188
+
189
+ ### On-Demand Content Fetching
190
+
191
+ For static or archived content, use `fetch()` which returns either a `ReadableStream<MoqtObject>` or a `FetchError`:
192
+
193
+ #### Fetch Implementation
194
+
195
+ Fetch operations are optimized for reliable delivery of static content:
196
+
197
+ - **Single Stream**: All requested objects are delivered sequentially in a single stream
198
+ - **Reliable Delivery**: Uses QUIC streams for guaranteed, ordered delivery
199
+ - **No Cancellation**: All requested objects are delivered as they provide historical data
200
+
201
+ ```typescript
202
+ const fetch = new Fetch(
203
+ client.nextClientRequestId,
204
+ trackAlias,
205
+ fullTrackName,
206
+ subscriberId,
207
+ startGroup, // Starting group ID
208
+ startObject, // Starting object ID
209
+ endGroup, // Ending group ID
210
+ endObject, // Ending object ID
211
+ authInfo,
212
+ )
213
+
214
+ const result = await client.fetch(fetch)
215
+
216
+ if (result instanceof FetchError) {
217
+ console.error(`Fetch failed: ${result.reasonPhrase}`)
218
+ // Handle fetch error
219
+ } else {
220
+ // Success - result is ReadableStream<MoqtObject>
221
+ const objectStream = result
222
+ const reader = objectStream.getReader()
223
+
224
+ try {
225
+ while (true) {
226
+ const { done, value: object } = await reader.read()
227
+ if (done) break
228
+
229
+ // Process fetched object
230
+ processObject(object)
231
+ }
232
+ } finally {
233
+ reader.releaseLock()
234
+ }
235
+ }
236
+ ```
237
+
238
+ ### Content Processing
239
+
240
+ Once you have the stream, process each `MoqtObject` based on its status:
241
+
242
+ ```typescript
243
+ function processObject(object: MoqtObject) {
244
+ // Check object status
245
+ switch (object.objectStatus) {
246
+ case ObjectStatus.Normal:
247
+ // Regular data object with payload
248
+ if (object.payload) {
249
+ processData(object.payload)
250
+ }
251
+ break
252
+ case ObjectStatus.ObjectDoesNotExist:
253
+ // Object was not available
254
+ handleMissingObject(object.groupId, object.objectId)
255
+ break
256
+ case ObjectStatus.GroupDoesNotExist:
257
+ // Entire group was not available
258
+ handleMissingGroup(object.groupId)
259
+ break
260
+ case ObjectStatus.EndOfGroup:
261
+ // Marks the end of a group
262
+ finalizeGroup(object.groupId)
263
+ break
264
+ case ObjectStatus.EndOfTrack:
265
+ // Marks the end of the track
266
+ finalizeTrack()
267
+ break
268
+ }
269
+ }
270
+ ```
271
+
272
+ ### Subscription Management
273
+
274
+ #### Subscription Lifecycle
275
+
276
+ ```typescript
277
+ // Create and send subscription
278
+ const subscribe = new Subscribe(/*...*/)
279
+ const result = await client.subscribe(subscribe)
280
+
281
+ if (result instanceof SubscribeError) {
282
+ console.error(`Subscription failed: ${result.reasonPhrase}`)
283
+ } else {
284
+ console.log('Subscription successful, processing stream...')
285
+ // Process the stream as shown above
286
+ }
287
+
288
+ // Unsubscribe when done
289
+ await client.unsubscribe(subscribeId)
290
+ ```
291
+
292
+ #### Subscription Updates
293
+
294
+ For live content, you can update the subscription range dynamically:
295
+
296
+ ```typescript
297
+ const subscribeUpdate = new SubscribeUpdate(
298
+ subscribeId,
299
+ startGroup, // New start group
300
+ startObject, // New start object
301
+ endGroup, // New end group (optional)
302
+ endObject, // New end object (optional)
303
+ subscriberPriority, // New priority (optional)
304
+ )
305
+
306
+ await client.subscribeUpdate(subscribeUpdate)
307
+ ```
308
+
309
+ ### Complete Subscriber Example
310
+
311
+ ```typescript
312
+ import { MOQtailClient } from './client/client'
313
+ import { PullPlayoutBuffer } from './util/pull_playout_buffer'
314
+
315
+ async function createSubscriber() {
316
+ // Initialize client
317
+ const client = await MOQtailClient.new(clientSetup, webTransport)
318
+
319
+ // Subscribe to live video
320
+ const subscribe = new Subscribe(
321
+ client.nextClientRequestId,
322
+ 1n, // trackAlias
323
+ FullTrackName.tryNew('live/conference', 'video'),
324
+ generateSubscriberId(),
325
+ null,
326
+ null, // Latest content
327
+ null,
328
+ null, // Ongoing
329
+ null, // No auth
330
+ )
331
+
332
+ const result = await client.subscribe(subscribe)
333
+
334
+ if (result instanceof SubscribeError) {
335
+ console.error(`Failed to subscribe: ${result.reasonPhrase}`)
336
+ return
337
+ }
338
+
339
+ // Set up playout buffer with the stream
340
+ const playoutBuffer = new PullPlayoutBuffer(result, {
341
+ bucketCapacity: 50,
342
+ targetLatencyMs: 500,
343
+ maxLatencyMs: 2000,
344
+ })
345
+
346
+ // Consumer-driven playout
347
+ const playoutLoop = () => {
348
+ playoutBuffer.nextObject((nextObject) => {
349
+ if (nextObject) {
350
+ // Decode and render the frame
351
+ decodeAndRender(nextObject)
352
+ }
353
+ requestAnimationFrame(playoutLoop)
354
+ })
355
+ }
356
+
357
+ // Start playout
358
+ requestAnimationFrame(playoutLoop)
359
+
360
+ return client
361
+ }
362
+ ```
363
+
364
+ ### Other Client Operations
365
+
366
+ The MOQtail client supports additional operations for track discovery and status management:
367
+
368
+ #### PublishNamespace Operations
369
+
370
+ Publishers use announce operations to make their tracks discoverable:
371
+
372
+ ```typescript
373
+ // PublishNamespace a namespace
374
+ const announce = new PublishNamespace(
375
+ client.nextClientRequestId,
376
+ Tuple.tryNew(['live', 'conference']), // Track namespace
377
+ )
378
+
379
+ const result = await client.publishNamespace(announce)
380
+ if (result instanceof PublishNamespaceError) {
381
+ console.error(`Publishing the namespace failed: ${result.reasonPhrase}`)
382
+ } else {
383
+ console.log('Namespace published successfully')
384
+ }
385
+
386
+ // Stop announcing a namespace
387
+ const publish_namespace_done = new publishNamespaceDone(Tuple.tryNew(['live', 'conference']))
388
+ await client.publishNamespaceDone(publish_namespace_done)
389
+ ```
390
+
391
+ #### Subscribe to Announcements
392
+
393
+ Subscribers can discover available tracks by subscribing to announcements:
394
+
395
+ ```typescript
396
+ // Subscribe to announcements for a namespace prefix
397
+ const subscribeNamespace = new SubscribeNamespace(
398
+ Tuple.tryNew(['live']), // Namespace prefix
399
+ )
400
+ await client.subscribeNamespace(subscribeNamespace)
401
+
402
+ // The client will now receive announce messages for tracks
403
+ // matching the 'live' prefix through its announcement handling
404
+
405
+ // Stop subscribing to announcements
406
+ const unsubscribeNamespace = new UnsubscribeNamespace(Tuple.tryNew(['live']))
407
+ await client.unsubscribeNamespace(unsubscribeNamespace)
408
+ ```
409
+
410
+ #### Track Status Requests
411
+
412
+ Query the status of specific tracks:
413
+
414
+ ```typescript
415
+ const trackStatus = new TrackStatusMessage(client.nextClientRequestId, FullTrackName.tryNew('live/conference', 'video'))
416
+
417
+ const result = await client.trackStatus(trackStatus)
418
+ if (result instanceof TrackStatusError) {
419
+ console.error(`Track status request failed: ${result.reasonPhrase}`)
420
+ } else {
421
+ // result is TrackStatus
422
+ console.log(`Track status: ${result.statusCode}`)
423
+ console.log(`Last group: ${result.lastGroup}`)
424
+ console.log(`Last object: ${result.lastObject}`)
425
+ }
426
+ ```
427
+
428
+ ## Utilities
429
+
430
+ The MOQtail library provides several utility classes to help with common streaming scenarios:
431
+
432
+ ### Playout Buffer
433
+
434
+ The `PullPlayoutBuffer` provides consumer-driven playout with GOP-aware buffering for smooth media playback:
435
+
436
+ ```typescript
437
+ import { PullPlayoutBuffer } from './util/pull_playout_buffer'
438
+
439
+ const playoutBuffer = new PullPlayoutBuffer(objectStream, {
440
+ bucketCapacity: 50, // Max objects in buffer (default: 50)
441
+ targetLatencyMs: 500, // Target latency in ms (default: 500)
442
+ maxLatencyMs: 2000, // Max latency before dropping GOPs (default: 2000)
443
+ })
444
+
445
+ // Consumer-driven object retrieval
446
+ playoutBuffer.nextObject((nextObject) => {
447
+ if (nextObject) {
448
+ // Process the object (decode, render, etc.)
449
+ processFrame(nextObject)
450
+ }
451
+ })
452
+
453
+ // Check buffer status
454
+ const status = playoutBuffer.getStatus()
455
+ console.log(`Buffer size: ${status.bufferSize}, Running: ${status.isRunning}`)
456
+ ```
457
+
458
+ **Key Features:**
459
+
460
+ - **GOP-Aware**: Automatically detects and manages Group of Pictures boundaries
461
+ - **Smart Eviction**: Drops entire GOPs when buffer is full to maintain decodable content
462
+ - **Consumer-Driven**: Pull-based API eliminates rate guessing and provides natural backpressure
463
+ - **Latency Management**: Automatically manages buffer size to maintain target latency
464
+
465
+ ### Network Telemetry
466
+
467
+ The `NetworkTelemetry` class provides real-time network performance monitoring:
468
+
469
+ ```typescript
470
+ import { NetworkTelemetry } from './util/telemetry'
471
+
472
+ const telemetry = new NetworkTelemetry(1000) // 1-second sliding window
473
+
474
+ // Report network events
475
+ telemetry.push({
476
+ latency: 50, // Round-trip time in ms
477
+ size: 1024, // Bytes transferred
478
+ })
479
+
480
+ // Get current metrics
481
+ console.log(`Throughput: ${telemetry.throughput} bytes/sec`)
482
+ console.log(`Average latency: ${telemetry.latency} ms`)
483
+ ```
484
+
485
+ **Use Cases:**
486
+
487
+ - Adaptive bitrate streaming decisions
488
+ - Network condition monitoring
489
+ - Performance debugging and optimization
490
+ - Quality of service reporting
491
+
492
+ ### Clock Synchronization
493
+
494
+ The `AkamaiOffset` utility provides clock synchronization with Akamai's time service:
495
+
496
+ ```typescript
497
+ import { AkamaiOffset } from './util/get_akamai_offset'
498
+
499
+ // Get clock skew relative to Akamai time servers
500
+ const clockSkew = await AkamaiOffset.getClockSkew()
501
+ console.log(`Local clock is ${clockSkew}ms ahead of network time`)
502
+
503
+ // Adjust local timestamps for network synchronization
504
+ const networkTime = Date.now() - clockSkew
505
+ ```
506
+
507
+ **Features:**
508
+
509
+ - **Network Time Synchronization**: Aligns local time with network time servers
510
+ - **RTT Compensation**: Accounts for round-trip time in synchronization calculations
511
+ - **Cached Results**: Subsequent calls return cached offset for performance
512
+ - **Media Synchronization**: Essential for multi-source media synchronization
513
+
514
+ These utilities work together to provide a robust foundation for real-time media streaming applications, handling the complex aspects of buffering, network monitoring, and time synchronization.