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 +514 -0
- package/dist/byte_buffer-BOK6VPTF.d.cts +864 -0
- package/dist/byte_buffer-BOK6VPTF.d.ts +864 -0
- package/dist/client/index.cjs +7785 -0
- package/dist/client/index.d.cts +1665 -0
- package/dist/client/index.d.ts +1665 -0
- package/dist/client/index.js +7767 -0
- package/dist/index.cjs +8559 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +8459 -0
- package/dist/model/index.cjs +4759 -0
- package/dist/model/index.d.cts +362 -0
- package/dist/model/index.d.ts +362 -0
- package/dist/model/index.js +4645 -0
- package/dist/util/index.cjs +1799 -0
- package/dist/util/index.d.cts +205 -0
- package/dist/util/index.d.ts +205 -0
- package/dist/util/index.js +1789 -0
- package/dist/version_parameter-CgEPNuUt.d.ts +1660 -0
- package/dist/version_parameter-DCE9_itC.d.cts +1660 -0
- package/package.json +97 -0
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.
|