stinky-moq-js 0.1.19 → 0.1.20
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 +310 -213
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +34 -32
- package/dist/index.mjs.map +1 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,84 +22,109 @@ npm install stinky-moq-js
|
|
|
22
22
|
## Quick Start
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
|
-
import {
|
|
25
|
+
import { MoqSessionBroadcaster, MoqSessionSubscriber } from 'stinky-moq-js';
|
|
26
26
|
|
|
27
|
-
// Create a
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
// Create a broadcaster
|
|
28
|
+
const broadcaster = new MoqSessionBroadcaster(
|
|
29
|
+
{
|
|
30
|
+
relayUrl: 'https://relay.quic.video',
|
|
31
|
+
namespace: 'my-app/user-123'
|
|
32
|
+
},
|
|
33
|
+
[
|
|
34
|
+
{ trackName: 'video', priority: 10, type: 'video' },
|
|
35
|
+
{ trackName: 'audio', priority: 5, type: 'audio' }
|
|
36
|
+
]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Create a subscriber
|
|
40
|
+
const subscriber = new MoqSessionSubscriber(
|
|
41
|
+
{
|
|
42
|
+
relayUrl: 'https://relay.quic.video',
|
|
43
|
+
namespace: 'my-app/user-456'
|
|
44
|
+
},
|
|
45
|
+
[
|
|
46
|
+
{ trackName: 'video', priority: 10 },
|
|
47
|
+
{ trackName: 'audio', priority: 5 }
|
|
48
|
+
]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Listen to events
|
|
52
|
+
broadcaster.on('stateChange', (status) => {
|
|
53
|
+
console.log('Broadcaster state:', status.state);
|
|
34
54
|
});
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log('Session state:', status.state);
|
|
56
|
+
subscriber.on('data', (trackName, data) => {
|
|
57
|
+
console.log(`Received ${data.length} bytes from ${trackName}`);
|
|
39
58
|
});
|
|
40
59
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
// Connect
|
|
61
|
+
await broadcaster.connect();
|
|
62
|
+
await subscriber.connect();
|
|
44
63
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
console.log('Found broadcast:', announcement.path);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Connect to the relay
|
|
51
|
-
await session.connect();
|
|
64
|
+
// Send data
|
|
65
|
+
broadcaster.send('video', videoData, true);
|
|
52
66
|
```
|
|
53
67
|
|
|
54
68
|
## Working with Broadcasts
|
|
55
69
|
|
|
56
70
|
```typescript
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
// Create a broadcaster with track configurations
|
|
72
|
+
const broadcaster = new MoqSessionBroadcaster(
|
|
73
|
+
{
|
|
74
|
+
relayUrl: 'https://relay.quic.video',
|
|
75
|
+
namespace: 'my-app/user-123'
|
|
76
|
+
},
|
|
77
|
+
[
|
|
78
|
+
{ trackName: 'video', priority: 10, type: 'video' },
|
|
79
|
+
{ trackName: 'audio', priority: 5, type: 'audio' }
|
|
80
|
+
]
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Listen to broadcast events
|
|
84
|
+
broadcaster.on('broadcastStateChange', (trackName, status) => {
|
|
68
85
|
console.log(`Broadcast ${trackName} state:`, status.state);
|
|
69
86
|
console.log('Bytes sent:', status.bytesSent);
|
|
70
87
|
});
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
broadcaster.on('broadcastError', (trackName, error) => {
|
|
73
90
|
console.error(`Broadcast error on ${trackName}:`, error.message);
|
|
74
91
|
});
|
|
75
92
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
await session.send('my-video-track', data);
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Working with Subscriptions
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
// Define what you want to subscribe to
|
|
85
|
-
const subscriptions = [
|
|
86
|
-
{ trackName: 'remote-video-track', priority: 1 },
|
|
87
|
-
{ trackName: 'remote-audio-track', priority: 2, retry: { delay: 2000 } }
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
// Apply the configuration
|
|
91
|
-
session.updateSubscriptions(subscriptions);
|
|
93
|
+
// Connect and send data
|
|
94
|
+
await broadcaster.connect();
|
|
92
95
|
|
|
93
|
-
//
|
|
94
|
-
|
|
96
|
+
// Send binary data (newGroup: true for keyframes/new groups)
|
|
97
|
+
const videoData = new Uint8Array([1, 2, 3, 4, 5]);
|
|
98
|
+
broCreate a subscriber with track configurations
|
|
99
|
+
const subscriber = new MoqSessionSubscriber(
|
|
100
|
+
{
|
|
101
|
+
relayUrl: 'https://relay.quic.video',
|
|
102
|
+
namespace: 'my-app/user-456'
|
|
103
|
+
},
|
|
104
|
+
[
|
|
105
|
+
{ trackName: 'video', priority: 10 },
|
|
106
|
+
{ trackName: 'audio', priority: 5, retry: { delay: 2000 } }
|
|
107
|
+
]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Listen to incoming data
|
|
111
|
+
subscriber.on('data', (trackName, data) => {
|
|
95
112
|
console.log('Received data from:', trackName);
|
|
96
113
|
console.log('Data length:', data.length);
|
|
97
114
|
});
|
|
98
115
|
|
|
99
|
-
// Listen to subscription status
|
|
100
|
-
|
|
116
|
+
// Listen to subscription status
|
|
117
|
+
subscriber.on('subscriptionStateChange', (trackName, status) => {
|
|
101
118
|
console.log(`Subscription ${trackName} state:`, status.state);
|
|
102
119
|
console.log('Bytes received:', status.bytesReceived);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
subscriber.on('subscriptionError', (trackName, error) => {
|
|
123
|
+
console.error(`Subscription error on ${trackName}:`, error.message);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Connect to start receiving data
|
|
127
|
+
await subscriber.connect( console.log('Bytes received:', status.bytesReceived);
|
|
103
128
|
console.log('Retry attempts:', status.retryAttempts);
|
|
104
129
|
});
|
|
105
130
|
|
|
@@ -120,6 +145,8 @@ interface MoQSessionConfig {
|
|
|
120
145
|
delay?: number; // Delay between attempts in ms (default: 1000)
|
|
121
146
|
};
|
|
122
147
|
connectionTimeout?: number; // Connection timeout in ms (default: 10000)
|
|
148
|
+
discoveryOnly?: boolean; // (Subscriber only) Only discover broadcasts,
|
|
149
|
+
// don't auto-subscribe (default: false)
|
|
123
150
|
}
|
|
124
151
|
```
|
|
125
152
|
|
|
@@ -129,6 +156,7 @@ interface MoQSessionConfig {
|
|
|
129
156
|
interface BroadcastConfig {
|
|
130
157
|
trackName: string; // Track name for the broadcast
|
|
131
158
|
priority?: number; // Priority for the track (default: 0)
|
|
159
|
+
type: 'video' | 'audio' | 'data'; // Type of track
|
|
132
160
|
}
|
|
133
161
|
```
|
|
134
162
|
|
|
@@ -149,7 +177,7 @@ interface SubscriptionConfig {
|
|
|
149
177
|
### Session Status
|
|
150
178
|
|
|
151
179
|
```typescript
|
|
152
|
-
const status =
|
|
180
|
+
const status = broadcaster.status; // or subscriber.status
|
|
153
181
|
console.log({
|
|
154
182
|
state: status.state, // SessionState enum
|
|
155
183
|
reconnectAttempts: status.reconnectAttempts,
|
|
@@ -161,30 +189,29 @@ console.log({
|
|
|
161
189
|
### Broadcast Status
|
|
162
190
|
|
|
163
191
|
```typescript
|
|
164
|
-
|
|
165
|
-
|
|
192
|
+
// Broadcasters track individual broadcast status
|
|
193
|
+
broadcaster.on('broadcastStateChange', (trackName, status) => {
|
|
166
194
|
console.log({
|
|
167
195
|
state: status.state, // BroadcastState enum
|
|
168
196
|
trackName: status.trackName,
|
|
169
197
|
bytesSent: status.bytesSent,
|
|
170
198
|
lastError: status.lastError
|
|
171
199
|
});
|
|
172
|
-
}
|
|
200
|
+
});
|
|
173
201
|
```
|
|
174
202
|
|
|
175
203
|
### Subscription Status
|
|
176
204
|
|
|
177
205
|
```typescript
|
|
178
|
-
|
|
179
|
-
|
|
206
|
+
// Subscribers track individual subscription status
|
|
207
|
+
subscriber.on('subscriptionStateChange', (trackName, status) => {
|
|
180
208
|
console.log({
|
|
181
209
|
state: status.state, // SubscriptionState enum
|
|
182
210
|
trackName: status.trackName,
|
|
183
211
|
bytesReceived: status.bytesReceived,
|
|
184
|
-
retryAttempts: status.retryAttempts,
|
|
185
212
|
lastError: status.lastError
|
|
186
213
|
});
|
|
187
|
-
}
|
|
214
|
+
});
|
|
188
215
|
```
|
|
189
216
|
|
|
190
217
|
## States
|
|
@@ -203,27 +230,26 @@ if (status) {
|
|
|
203
230
|
|
|
204
231
|
### Subscription States
|
|
205
232
|
- `PENDING`: Waiting to subscribe
|
|
206
|
-
- `
|
|
207
|
-
- `
|
|
208
|
-
- `FAILED`: Subscription failed
|
|
209
|
-
|
|
233
|
+
- `IDLE`: Not subscribed
|
|
234
|
+
- `SUBSCRIBED`: Successfully subscribed and receiving data
|
|
235
|
+
- `FAILED`: Subscription failed
|
|
210
236
|
## Error Handling
|
|
211
237
|
|
|
212
238
|
```typescript
|
|
213
239
|
// Session errors
|
|
214
240
|
session.on('error', (error) => {
|
|
215
241
|
console.error('Session error:', error.message);
|
|
242
|
+
broadcaster.on('error', (error) => {
|
|
243
|
+
console.error('Session error:', error.message);
|
|
216
244
|
});
|
|
217
245
|
|
|
218
246
|
// Broadcast errors (per track)
|
|
219
|
-
|
|
247
|
+
broadcaster.on('broadcastError', (trackName, error) => {
|
|
220
248
|
console.error(`Broadcast error on ${trackName}:`, error.message);
|
|
221
249
|
});
|
|
222
250
|
|
|
223
251
|
// Subscription errors (per track)
|
|
224
|
-
|
|
225
|
-
console.error(`Subscription error on ${trackName}:`, error.message);
|
|
226
|
-
});
|
|
252
|
+
subscriber
|
|
227
253
|
```
|
|
228
254
|
|
|
229
255
|
## Cleanup
|
|
@@ -231,145 +257,191 @@ session.on('subscriptionError', (trackName, error) => {
|
|
|
231
257
|
```typescript
|
|
232
258
|
// Remove all broadcasts and subscriptions
|
|
233
259
|
session.updateBroadcasts([]); // Empty array removes all
|
|
234
|
-
|
|
260
|
+
sesDisconnect and cleanup
|
|
261
|
+
broadcaster.disconnect();
|
|
262
|
+
subscriber.dispose
|
|
263
|
+
## Broadcast Discovery
|
|
235
264
|
|
|
236
|
-
|
|
237
|
-
await session.disconnect();
|
|
238
|
-
```
|
|
265
|
+
The library automatically discovers available broadcasts via MoQ announcements. This is essential for building multi-user applications like video chat rooms.
|
|
239
266
|
|
|
240
|
-
|
|
267
|
+
### Discovery-Only Mode (Room-Level Discovery)
|
|
241
268
|
|
|
242
|
-
|
|
269
|
+
Use `discoveryOnly: true` to discover broadcasts without automatically subscribing. This is perfect for room-level discovery where you want to find all users in a room and then selectively subscribe to them:
|
|
243
270
|
|
|
244
271
|
```typescript
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
272
|
+
// Create a discovery subscriber for a room
|
|
273
|
+
const roomDiscovery = new MoqSessionSubscriber(
|
|
274
|
+
{
|
|
275
|
+
relayUrl: 'https://relay.quic.video',
|
|
276
|
+
namespace: 'lobby', // Room prefix
|
|
277
|
+
discoveryOnly: true // Only discover, don't auto-subscribe
|
|
278
|
+
},
|
|
279
|
+
[] // No subscriptions needed for discovery
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Listen for user announcements
|
|
283
|
+
roomDiscovery.on('broadcastAnnounced', (announcement) => {
|
|
284
|
+
console.log('User broadcast:', announcement.path); // e.g., "lobby/user/abc123"
|
|
248
285
|
console.log('Is active:', announcement.active);
|
|
249
286
|
|
|
250
|
-
// Subscribe to discovered broadcast
|
|
251
287
|
if (announcement.active) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
288
|
+
// Manually create a subscription to this user's tracks
|
|
289
|
+
const userSubscriber = new MoqSessionSubscriber(
|
|
290
|
+
{
|
|
291
|
+
relayUrl: 'https://relay.quic.video',
|
|
292
|
+
namespace: announcement.path, // e.g., "lobby/user/abc123"
|
|
293
|
+
discoveryOnly: false
|
|
294
|
+
},
|
|
295
|
+
[
|
|
296
|
+
{ trackName: 'video', priority: 10 },
|
|
297
|
+
{ trackName: 'audio', priority: 5 }
|
|
298
|
+
]
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
await userSubscriber.connect();
|
|
302
|
+
} else {
|
|
303
|
+
// User left - clean up their subscription
|
|
304
|
+
console.log('User left:', announcement.path);
|
|
255
305
|
}
|
|
256
306
|
});
|
|
257
|
-
```
|
|
258
307
|
|
|
259
|
-
|
|
308
|
+
await roomDiscovery.connect();
|
|
309
|
+
```
|
|
260
310
|
|
|
261
|
-
|
|
311
|
+
### Multi-User Video Chat Room Example
|
|
262
312
|
|
|
263
|
-
|
|
313
|
+
Here's a complete example of building a video chat room with discovery:
|
|
264
314
|
|
|
265
315
|
```typescript
|
|
266
|
-
//
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
{ trackName: 'audio', priority: 2 },
|
|
270
|
-
{ trackName: 'metadata', priority: 0 }
|
|
271
|
-
];
|
|
272
|
-
|
|
273
|
-
// Apply the desired state - library handles add/remove/update automatically
|
|
274
|
-
session.updateBroadcasts(desiredBroadcasts);
|
|
275
|
-
|
|
276
|
-
// Later, change the desired state
|
|
277
|
-
const newBroadcasts = [
|
|
278
|
-
{ trackName: 'video', priority: 1 }, // Kept
|
|
279
|
-
{ trackName: 'chat', priority: 0 } // Audio and metadata removed, chat added
|
|
280
|
-
];
|
|
281
|
-
|
|
282
|
-
session.updateBroadcasts(newBroadcasts);
|
|
283
|
-
```
|
|
316
|
+
// Namespace structure: {room}/user/{userId}
|
|
317
|
+
const roomName = 'lobby';
|
|
318
|
+
const myUserId = 'user-123';
|
|
284
319
|
|
|
285
|
-
|
|
320
|
+
// 1. Create a broadcaster for your own tracks
|
|
321
|
+
const myBroadcaster = new MoqSessionBroadcaster(
|
|
322
|
+
{
|
|
323
|
+
relayUrl: 'https://relay.quic.video',
|
|
324
|
+
namespace: `${roomName}/user/${myUserId}`
|
|
325
|
+
},
|
|
326
|
+
[
|
|
327
|
+
{ trackName: 'video', priority: 10, type: 'video' },
|
|
328
|
+
{ trackName: 'audio', priority: 5, type: 'audio' }
|
|
329
|
+
]
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
await myBroadcaster.connect();
|
|
333
|
+
|
|
334
|
+
// 2. Create a discovery subscriber to find other users
|
|
335
|
+
const roomDiscovery = new MoqSessionSubscriber(
|
|
336
|
+
{
|
|
337
|
+
relayUrl: 'https://relay.quic.video',
|
|
338
|
+
namespace: roomName,
|
|
339
|
+
discoveryOnly: true
|
|
340
|
+
},
|
|
341
|
+
[]
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// Track active users
|
|
345
|
+
const activeUsers = new Map();
|
|
346
|
+
|
|
347
|
+
roomDiscovery.on('broadcastAnnounced', async (announcement) => {
|
|
348
|
+
const userPath = announcement.path; // e.g., "lobby/user/xyz789"
|
|
349
|
+
|
|
350
|
+
// Skip our own broadcast
|
|
351
|
+
if (userPath === `${roomName}/user/${myUserId}`) return;
|
|
352
|
+
|
|
353
|
+
if (announcement.active) {
|
|
354
|
+
// New user joined - subscribe to their tracks
|
|
355
|
+
const userSubscriber = new MoqSessionSubscriber(
|
|
356
|
+
{
|
|
357
|
+
relayUrl: 'https://relay.quic.video',
|
|
358
|
+
namespace: userPath
|
|
359
|
+
},
|
|
360
|
+
[
|
|
361
|
+
{ trackName: 'video', priority: 10 },
|
|
362
|
+
{ trackName: 'audio', priority: 5 }
|
|
363
|
+
]
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
userSubscriber.on('data', (trackName, data) => {
|
|
367
|
+
console.log(`Received ${trackName} from ${userPath}:`, data.length, 'bytes');
|
|
368
|
+
// Process video/audio data
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await userSubscriber.connect();
|
|
372
|
+
activeUsers.set(userPath, userSubscriber);
|
|
373
|
+
|
|
374
|
+
console.log(`User joined: ${userPath}`);
|
|
375
|
+
} else {
|
|
376
|
+
// User left - clean up
|
|
377
|
+
const userSubscriber = activeUsers.get(userPath);
|
|
378
|
+
if (userSubscriber) {
|
|
379
|
+
userSubscriber.dispose();
|
|
380
|
+
activeUsers.delete(userPath);
|
|
381
|
+
console.log(`User left: ${userPath}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
286
385
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
{ trackName: 'remote-chat', priority: 0 }
|
|
293
|
-
];
|
|
294
|
-
|
|
295
|
-
// Apply the desired state
|
|
296
|
-
session.updateSubscriptions(desiredSubscriptions);
|
|
297
|
-
|
|
298
|
-
// Update subscriptions based on user preferences
|
|
299
|
-
const userPreferences = {
|
|
300
|
-
videoEnabled: true,
|
|
301
|
-
audioEnabled: false,
|
|
302
|
-
chatEnabled: true
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
const activeSubscriptions = [
|
|
306
|
-
...(userPreferences.videoEnabled ? [{ trackName: 'remote-video', priority: 1 }] : []),
|
|
307
|
-
...(userPreferences.audioEnabled ? [{ trackName: 'remote-audio', priority: 2 }] : []),
|
|
308
|
-
...(userPreferences.chatEnabled ? [{ trackName: 'remote-chat', priority: 0 }] : [])
|
|
309
|
-
];
|
|
310
|
-
|
|
311
|
-
session.updateSubscriptions(activeSubscriptions);
|
|
386
|
+
await roomDiscovery.connect();
|
|
387
|
+
|
|
388
|
+
// 3. Broadcast your video/audio
|
|
389
|
+
myBroadcaster.send('video', videoData, true);
|
|
390
|
+
myBroadcaster.send('audio', audioData, false);
|
|
312
391
|
```
|
|
313
392
|
|
|
314
|
-
###
|
|
393
|
+
### How Discovery Works
|
|
315
394
|
|
|
316
|
-
|
|
317
|
-
- **Automatic Cleanup**: Tracks not in the desired state are automatically removed
|
|
318
|
-
- **Idempotent**: Calling `updateBroadcasts`/`updateSubscriptions` multiple times with the same config is safe
|
|
319
|
-
- **Less Error-Prone**: No need to manually track what's been added/removed
|
|
320
|
-
- **Reactive Updates**: Easy to update based on user preferences or external state changes
|
|
395
|
+
The discovery mechanism uses the MoQ `announced()` method under the hood:
|
|
321
396
|
|
|
322
|
-
|
|
397
|
+
- **Discovery uses `announced(prefix)`**: Listens for all broadcasts matching a prefix (e.g., `lobby`)
|
|
398
|
+
- **Subscriptions use `consume(path)`**: Subscribes to tracks from a specific broadcast path (e.g., `lobby/user/abc123`)
|
|
399
|
+
- **These are separate operations**: Discovery doesn't require consuming, allowing efficient room-level discovery
|
|
400
|
+
- **Active/Inactive events**: Announcements include an `active` flag - `true` when a broadcast starts, `false` when it ends
|
|
401
|
+
|
|
402
|
+
## Dynamic Track Management
|
|
323
403
|
|
|
324
|
-
|
|
404
|
+
You can add or remove subscriptions dynamically:
|
|
325
405
|
|
|
326
406
|
```typescript
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
{ trackName: '
|
|
336
|
-
{ trackName: '
|
|
337
|
-
|
|
338
|
-
];
|
|
339
|
-
|
|
340
|
-
session.updateBroadcasts(broadcasts);
|
|
341
|
-
session.updateSubscriptions(subscriptions);
|
|
342
|
-
|
|
343
|
-
// Get all active track names
|
|
344
|
-
const allBroadcasts = session.getAllBroadcastTrackNames();
|
|
345
|
-
const allSubscriptions = session.getAllSubscriptionTrackNames();
|
|
346
|
-
console.log('Broadcasting:', allBroadcasts);
|
|
347
|
-
console.log('Subscribed to:', allSubscriptions);
|
|
407
|
+
// Add a subscription
|
|
408
|
+
subscriber.addSubscription({ trackName: 'chat', priority: 0 });
|
|
409
|
+
|
|
410
|
+
// Remove a subscription
|
|
411
|
+
subscriber.removeSubscription('chat');
|
|
412
|
+
|
|
413
|
+
// Update all subscriptions at once
|
|
414
|
+
subscriber.updateSubscriptions([
|
|
415
|
+
{ trackName: 'video', priority: 10 },
|
|
416
|
+
{ trackName: 'audio', priority: 5 }
|
|
417
|
+
]);
|
|
348
418
|
```
|
|
349
419
|
|
|
420
|
+
## Advanced Usage
|
|
421
|
+
|
|
350
422
|
### Infinite Retry Logic
|
|
351
423
|
|
|
352
424
|
Both reconnection and subscription retries run infinitely with configurable delays:
|
|
353
425
|
|
|
354
426
|
```typescript
|
|
355
427
|
// Session with custom reconnection delay
|
|
356
|
-
const
|
|
357
|
-
relayUrl: 'wss://relay.quic.video',
|
|
358
|
-
namespace: 'persistent-app',
|
|
359
|
-
reconnection: {
|
|
360
|
-
delay: 5000 // Wait 5 seconds between reconnection attempts
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
// Subscription with custom retry delay
|
|
365
|
-
session.updateSubscriptions([
|
|
428
|
+
const subscriber = new MoqSessionSubscriber(
|
|
366
429
|
{
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
430
|
+
relayUrl: 'https://relay.quic.video',
|
|
431
|
+
namespace: 'persistent-app',
|
|
432
|
+
reconnection: {
|
|
433
|
+
delay: 5000 // Wait 5 seconds between reconnection attempts
|
|
370
434
|
}
|
|
371
|
-
}
|
|
372
|
-
|
|
435
|
+
},
|
|
436
|
+
[
|
|
437
|
+
{
|
|
438
|
+
trackName: 'important-track',
|
|
439
|
+
retry: {
|
|
440
|
+
delay: 3000 // Wait 3 seconds between retry attempts
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
]
|
|
444
|
+
);
|
|
373
445
|
```
|
|
374
446
|
|
|
375
447
|
### Bidirectional Communication
|
|
@@ -400,47 +472,72 @@ sessionA.on('data', (trackName, data) => {
|
|
|
400
472
|
const sessionB = new MoQSession({
|
|
401
473
|
relayUrl: 'wss://relay.quic.video',
|
|
402
474
|
namespace: 'app-b'
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
await sessionB.connect();
|
|
475
|
+
});users:
|
|
406
476
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
477
|
+
```typescript
|
|
478
|
+
// User A: Broadcaster and Subscriber
|
|
479
|
+
const broadcasterA = new MoqSessionBroadcaster(
|
|
480
|
+
{
|
|
481
|
+
relayUrl: 'https://relay.quic.video',
|
|
482
|
+
namespace: 'app/user-a'
|
|
483
|
+
},
|
|
484
|
+
[
|
|
485
|
+
{ trackName: 'video', priority: 10, type: 'video' },
|
|
486
|
+
{ trackName: 'audio', priority: 5, type: 'audio' }
|
|
487
|
+
]
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const subscriberA = new MoqSessionSubscriber(
|
|
491
|
+
{
|
|
492
|
+
relayUrl: 'https://relay.quic.video',
|
|
493
|
+
namespace: 'app/user-b'
|
|
494
|
+
},
|
|
495
|
+
[
|
|
496
|
+
{ trackName: 'video', priority: 10 },
|
|
497
|
+
{ trackName: 'audio', priority: 5 }
|
|
498
|
+
]
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// User B: Broadcaster and Subscriber
|
|
502
|
+
const broadcasterB = new MoqSessionBroadcaster(
|
|
503
|
+
{
|
|
504
|
+
relayUrl: 'https://relay.quic.video',
|
|
505
|
+
namespace: 'app/user-b'
|
|
506
|
+
},
|
|
507
|
+
[
|
|
508
|
+
{ trackName: 'video', priority: 10, type: 'video' },
|
|
509
|
+
{ trackName: 'audio', priority: 5, type: 'audio' }
|
|
510
|
+
]
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const subscriberB = new MoqSessionSubscriber(
|
|
514
|
+
{
|
|
515
|
+
relayUrl: 'https://relay.quic.video',
|
|
516
|
+
namespace: 'app/user-a'
|
|
517
|
+
},
|
|
518
|
+
[
|
|
519
|
+
{ trackName: 'video', priority: 10 },
|
|
520
|
+
{ trackName: 'audio', priority: 5 }
|
|
521
|
+
]
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
// Connect all sessions
|
|
525
|
+
await Promise.all([
|
|
526
|
+
broadcasterA.connect(),
|
|
527
|
+
subscriberA.connect(),
|
|
528
|
+
broadcasterB.connect(),
|
|
529
|
+
subscriberB.connect()
|
|
530
|
+
]);
|
|
410
531
|
|
|
411
|
-
// Listen for data
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
console.log('App B received data from App A:', data);
|
|
415
|
-
}
|
|
532
|
+
// Listen for incoming data
|
|
533
|
+
subscriberA.on('data', (trackName, data) => {
|
|
534
|
+
console.log('User A received:', trackName, data.length);
|
|
416
535
|
});
|
|
417
536
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
// Send data from App B to App A
|
|
422
|
-
await sessionB.send('data-from-b', new TextEncoder().encode('Hello from B'));
|
|
423
|
-
```
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
## Dependencies
|
|
427
|
-
|
|
428
|
-
This library is built on top of:
|
|
429
|
-
- [@kixelated/moq](https://github.com/kixelated/moq-js) v0.9.1 - Core MoQ implementation
|
|
430
|
-
- Uses the `moq-lite-draft-04` specification (not the standard IETF MoQ draft)
|
|
431
|
-
- WebTransport only (no WebSocket fallback)
|
|
432
|
-
|
|
433
|
-
## Browser Support
|
|
434
|
-
|
|
435
|
-
Requires modern browsers with WebTransport support:
|
|
436
|
-
- Chrome 97+
|
|
437
|
-
- Edge 97+
|
|
438
|
-
- Safari 15.4+ (with WebTransport enabled)
|
|
439
|
-
|
|
440
|
-
## License
|
|
441
|
-
|
|
442
|
-
MIT
|
|
443
|
-
|
|
444
|
-
## Contributing
|
|
537
|
+
subscriberB.on('data', (trackName, data) => {
|
|
538
|
+
console.log('User B received:', trackName, data.length);
|
|
539
|
+
});
|
|
445
540
|
|
|
446
|
-
|
|
541
|
+
// Send data
|
|
542
|
+
broadcasterA.send('video', videoDataA, true);
|
|
543
|
+
broadcasterB.send('video', videoDataB, true
|