@marianmeres/webrtc 1.0.0 → 1.0.2
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/AGENTS.md +16 -0
- package/API.md +33 -0
- package/README.md +1 -0
- package/dist/types.d.ts +14 -1
- package/dist/webrtc-manager.js +127 -22
- package/package.json +3 -3
- package/llm.txt +0 -364
package/AGENTS.md
CHANGED
|
@@ -117,6 +117,21 @@ ERROR --RESET--> IDLE
|
|
|
117
117
|
new WebRtcManager(factory: WebRtcFactory, config?: WebRtcManagerConfig)
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
### Logger Interface
|
|
121
|
+
|
|
122
|
+
Console-compatible logger interface for custom logging implementations.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
interface Logger {
|
|
126
|
+
debug: (...args: any[]) => string;
|
|
127
|
+
log: (...args: any[]) => string;
|
|
128
|
+
warn: (...args: any[]) => string;
|
|
129
|
+
error: (...args: any[]) => string;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Each method returns a string representation of the first argument, enabling patterns like `throw new Error(logger.error("msg"))`.
|
|
134
|
+
|
|
120
135
|
### WebRtcFactory Interface
|
|
121
136
|
|
|
122
137
|
```typescript
|
|
@@ -138,6 +153,7 @@ interface WebRtcManagerConfig {
|
|
|
138
153
|
maxReconnectAttempts?: number; // Default: 5
|
|
139
154
|
reconnectDelay?: number; // Default: 1000ms
|
|
140
155
|
debug?: boolean; // Default: false
|
|
156
|
+
logger?: Logger; // Custom logger, falls back to console
|
|
141
157
|
}
|
|
142
158
|
```
|
|
143
159
|
|
package/API.md
CHANGED
|
@@ -681,6 +681,36 @@ console.log(manager.toMermaid());
|
|
|
681
681
|
|
|
682
682
|
## Types
|
|
683
683
|
|
|
684
|
+
### Logger
|
|
685
|
+
|
|
686
|
+
Console-compatible logger interface for custom logging implementations.
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
interface Logger {
|
|
690
|
+
debug: (...args: any[]) => string;
|
|
691
|
+
log: (...args: any[]) => string;
|
|
692
|
+
warn: (...args: any[]) => string;
|
|
693
|
+
error: (...args: any[]) => string;
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Each method accepts variadic arguments and returns a string representation of the first argument. This enables patterns like `throw new Error(logger.error("msg"))`.
|
|
698
|
+
|
|
699
|
+
**Example Custom Logger:**
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
import { clog } from '@marianmeres/clog';
|
|
703
|
+
|
|
704
|
+
const logger = clog('WebRTC');
|
|
705
|
+
|
|
706
|
+
const manager = new WebRtcManager(factory, {
|
|
707
|
+
debug: true,
|
|
708
|
+
logger: logger,
|
|
709
|
+
});
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
684
714
|
### WebRtcFactory
|
|
685
715
|
|
|
686
716
|
Interface for dependency injection of WebRTC primitives.
|
|
@@ -731,6 +761,9 @@ interface WebRtcManagerConfig {
|
|
|
731
761
|
|
|
732
762
|
/** Enable debug logging. Default: false */
|
|
733
763
|
debug?: boolean;
|
|
764
|
+
|
|
765
|
+
/** Custom logger instance. If not provided, falls back to console. */
|
|
766
|
+
logger?: Logger;
|
|
734
767
|
}
|
|
735
768
|
```
|
|
736
769
|
|
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ const manager = new WebRtcManager(factory, config);
|
|
|
52
52
|
- `maxReconnectAttempts`: Max reconnection attempts (default: 5)
|
|
53
53
|
- `reconnectDelay`: Initial reconnection delay in ms (default: 1000)
|
|
54
54
|
- `debug`: Enable debug logging (default: false)
|
|
55
|
+
- `logger`: Custom logger instance implementing `Logger` interface (default: console)
|
|
55
56
|
|
|
56
57
|
### State and Properties
|
|
57
58
|
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console-compatible logger interface.
|
|
3
|
+
* Each method accepts variadic arguments and returns a string representation of the first argument.
|
|
4
|
+
* This enables patterns like `throw new Error(logger.error("msg"))`.
|
|
5
|
+
*/
|
|
6
|
+
export interface Logger {
|
|
7
|
+
debug: (...args: any[]) => string;
|
|
8
|
+
log: (...args: any[]) => string;
|
|
9
|
+
warn: (...args: any[]) => string;
|
|
10
|
+
error: (...args: any[]) => string;
|
|
11
|
+
}
|
|
1
12
|
export interface WebRtcManagerConfig {
|
|
2
13
|
/** Initial peer configuration (ICE servers, etc.) */
|
|
3
14
|
peerConfig?: RTCConfiguration;
|
|
@@ -11,8 +22,10 @@ export interface WebRtcManagerConfig {
|
|
|
11
22
|
maxReconnectAttempts?: number;
|
|
12
23
|
/** Initial reconnection delay in ms. Doubles with each attempt. Defaults to 1000. */
|
|
13
24
|
reconnectDelay?: number;
|
|
14
|
-
/**
|
|
25
|
+
/** Enable debug logging. Defaults to false. */
|
|
15
26
|
debug?: boolean;
|
|
27
|
+
/** Custom logger instance. If not provided, falls back to console. */
|
|
28
|
+
logger?: Logger;
|
|
16
29
|
}
|
|
17
30
|
export interface WebRtcFactory {
|
|
18
31
|
createPeerConnection(config?: RTCConfiguration): RTCPeerConnection;
|
package/dist/webrtc-manager.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import { FSM } from "@marianmeres/fsm";
|
|
2
2
|
import { PubSub } from "@marianmeres/pubsub";
|
|
3
3
|
import { WebRtcState, WebRtcFsmEvent, } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Default console-based logger that wraps console methods to satisfy the Logger interface.
|
|
6
|
+
* Returns string representation of the first argument for chaining.
|
|
7
|
+
*/
|
|
8
|
+
const createDefaultLogger = () => ({
|
|
9
|
+
debug: (...args) => {
|
|
10
|
+
console.debug(...args);
|
|
11
|
+
return String(args[0] ?? "");
|
|
12
|
+
},
|
|
13
|
+
log: (...args) => {
|
|
14
|
+
console.log(...args);
|
|
15
|
+
return String(args[0] ?? "");
|
|
16
|
+
},
|
|
17
|
+
warn: (...args) => {
|
|
18
|
+
console.warn(...args);
|
|
19
|
+
return String(args[0] ?? "");
|
|
20
|
+
},
|
|
21
|
+
error: (...args) => {
|
|
22
|
+
console.error(...args);
|
|
23
|
+
return String(args[0] ?? "");
|
|
24
|
+
},
|
|
25
|
+
});
|
|
4
26
|
/**
|
|
5
27
|
* WebRTC connection manager with FSM-based lifecycle and event-driven architecture.
|
|
6
28
|
*
|
|
@@ -57,6 +79,7 @@ export class WebRtcManager {
|
|
|
57
79
|
#pc = null;
|
|
58
80
|
#factory;
|
|
59
81
|
#config;
|
|
82
|
+
#logger;
|
|
60
83
|
#localStream = null;
|
|
61
84
|
#remoteStream = null;
|
|
62
85
|
#dataChannels = new Map();
|
|
@@ -71,6 +94,7 @@ export class WebRtcManager {
|
|
|
71
94
|
constructor(factory, config = {}) {
|
|
72
95
|
this.#factory = factory;
|
|
73
96
|
this.#config = config;
|
|
97
|
+
this.#logger = config.logger ?? createDefaultLogger();
|
|
74
98
|
this.#pubsub = new PubSub();
|
|
75
99
|
// Initialize FSM
|
|
76
100
|
this.#fsm = new FSM({
|
|
@@ -193,7 +217,7 @@ export class WebRtcManager {
|
|
|
193
217
|
return devices.filter((d) => d.kind === "audioinput");
|
|
194
218
|
}
|
|
195
219
|
catch (e) {
|
|
196
|
-
|
|
220
|
+
this.#logger.error("[WebRtcManager] Failed to enumerate devices:", e);
|
|
197
221
|
return [];
|
|
198
222
|
}
|
|
199
223
|
}
|
|
@@ -204,7 +228,7 @@ export class WebRtcManager {
|
|
|
204
228
|
*/
|
|
205
229
|
async switchMicrophone(deviceId) {
|
|
206
230
|
if (!this.#pc || !this.#localStream) {
|
|
207
|
-
|
|
231
|
+
this.#logger.error("[WebRtcManager] Cannot switch microphone: not initialized or no active stream");
|
|
208
232
|
return false;
|
|
209
233
|
}
|
|
210
234
|
try {
|
|
@@ -240,7 +264,7 @@ export class WebRtcManager {
|
|
|
240
264
|
return true;
|
|
241
265
|
}
|
|
242
266
|
catch (e) {
|
|
243
|
-
|
|
267
|
+
this.#logger.error("[WebRtcManager] Failed to switch microphone:", e);
|
|
244
268
|
this.#error(e);
|
|
245
269
|
return false;
|
|
246
270
|
}
|
|
@@ -250,15 +274,20 @@ export class WebRtcManager {
|
|
|
250
274
|
* Must be called before creating offers or answers. Can only be called from IDLE state.
|
|
251
275
|
*/
|
|
252
276
|
async initialize() {
|
|
253
|
-
if (this.state !== WebRtcState.IDLE)
|
|
277
|
+
if (this.state !== WebRtcState.IDLE) {
|
|
278
|
+
this.#debug("initialize() called but state is not IDLE:", this.state);
|
|
254
279
|
return;
|
|
280
|
+
}
|
|
281
|
+
this.#debug("Initializing...");
|
|
255
282
|
this.#dispatch(WebRtcFsmEvent.INIT);
|
|
256
283
|
try {
|
|
257
284
|
this.#pc = this.#factory.createPeerConnection(this.#config.peerConfig);
|
|
285
|
+
this.#debug("Peer connection created");
|
|
258
286
|
this.#setupPcListeners();
|
|
259
287
|
// Setup device change detection now that we have a connection
|
|
260
288
|
this.#setupDeviceChangeListener();
|
|
261
289
|
if (this.#config.enableMicrophone) {
|
|
290
|
+
this.#debug("Enabling microphone (config enabled)");
|
|
262
291
|
const success = await this.enableMicrophone(true);
|
|
263
292
|
if (!success) {
|
|
264
293
|
this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
|
|
@@ -270,10 +299,13 @@ export class WebRtcManager {
|
|
|
270
299
|
// Always setup to receive audio, even if we don't enable microphone
|
|
271
300
|
// This ensures the SDP includes audio media line
|
|
272
301
|
this.#pc.addTransceiver("audio", { direction: "recvonly" });
|
|
302
|
+
this.#debug("Added recvonly audio transceiver");
|
|
273
303
|
}
|
|
274
304
|
if (this.#config.dataChannelLabel) {
|
|
305
|
+
this.#debug("Creating default data channel:", this.#config.dataChannelLabel);
|
|
275
306
|
this.createDataChannel(this.#config.dataChannelLabel);
|
|
276
307
|
}
|
|
308
|
+
this.#debug("Initialization complete");
|
|
277
309
|
}
|
|
278
310
|
catch (e) {
|
|
279
311
|
this.#error(e);
|
|
@@ -284,12 +316,15 @@ export class WebRtcManager {
|
|
|
284
316
|
* If disconnected, reinitializes the peer connection.
|
|
285
317
|
*/
|
|
286
318
|
async connect() {
|
|
319
|
+
this.#debug("connect() called, current state:", this.state);
|
|
287
320
|
// Initialize if needed
|
|
288
321
|
if (this.state === WebRtcState.IDLE) {
|
|
322
|
+
this.#debug("State is IDLE, initializing first");
|
|
289
323
|
await this.initialize();
|
|
290
324
|
}
|
|
291
325
|
// Reinitialize if disconnected (PeerConnection was closed)
|
|
292
326
|
if (this.state === WebRtcState.DISCONNECTED) {
|
|
327
|
+
this.#debug("State is DISCONNECTED, reinitializing");
|
|
293
328
|
// Clean up old connection
|
|
294
329
|
this.#cleanup();
|
|
295
330
|
// Reset to IDLE and reinitialize
|
|
@@ -299,8 +334,11 @@ export class WebRtcManager {
|
|
|
299
334
|
return;
|
|
300
335
|
}
|
|
301
336
|
if (this.state === WebRtcState.CONNECTED ||
|
|
302
|
-
this.state === WebRtcState.CONNECTING)
|
|
337
|
+
this.state === WebRtcState.CONNECTING) {
|
|
338
|
+
this.#debug("Already connected or connecting, skipping");
|
|
303
339
|
return;
|
|
340
|
+
}
|
|
341
|
+
this.#debug("Transitioning to CONNECTING");
|
|
304
342
|
this.#dispatch(WebRtcFsmEvent.CONNECT);
|
|
305
343
|
}
|
|
306
344
|
/**
|
|
@@ -309,14 +347,19 @@ export class WebRtcManager {
|
|
|
309
347
|
* @returns True if successful, false if failed to get user media.
|
|
310
348
|
*/
|
|
311
349
|
async enableMicrophone(enable) {
|
|
350
|
+
this.#debug("enableMicrophone() called:", enable);
|
|
312
351
|
if (enable) {
|
|
313
|
-
if (this.#localStream)
|
|
314
|
-
|
|
352
|
+
if (this.#localStream) {
|
|
353
|
+
this.#debug("Microphone already enabled");
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
315
356
|
try {
|
|
357
|
+
this.#debug("Requesting user media...");
|
|
316
358
|
const stream = await this.#factory.getUserMedia({
|
|
317
359
|
audio: true,
|
|
318
360
|
video: false,
|
|
319
361
|
});
|
|
362
|
+
this.#debug("User media obtained, tracks:", stream.getAudioTracks().length);
|
|
320
363
|
this.#localStream = stream;
|
|
321
364
|
this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, stream);
|
|
322
365
|
if (this.#pc) {
|
|
@@ -329,25 +372,31 @@ export class WebRtcManager {
|
|
|
329
372
|
await audioTransceiver.sender.replaceTrack(track);
|
|
330
373
|
// Update direction to sendrecv
|
|
331
374
|
audioTransceiver.direction = "sendrecv";
|
|
375
|
+
this.#debug("Replaced track in existing transceiver");
|
|
332
376
|
}
|
|
333
377
|
else {
|
|
334
378
|
// Add track normally
|
|
335
379
|
stream.getTracks().forEach((track) => {
|
|
336
380
|
this.#pc.addTrack(track, stream);
|
|
337
381
|
});
|
|
382
|
+
this.#debug("Added tracks to peer connection");
|
|
338
383
|
}
|
|
339
384
|
}
|
|
385
|
+
this.#debug("Microphone enabled successfully");
|
|
340
386
|
return true;
|
|
341
387
|
}
|
|
342
388
|
catch (e) {
|
|
343
|
-
|
|
389
|
+
this.#logger.error("[WebRtcManager] Failed to get user media:", e);
|
|
344
390
|
this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, { error: e });
|
|
345
391
|
return false;
|
|
346
392
|
}
|
|
347
393
|
}
|
|
348
394
|
else {
|
|
349
|
-
if (!this.#localStream)
|
|
395
|
+
if (!this.#localStream) {
|
|
396
|
+
this.#debug("Microphone already disabled");
|
|
350
397
|
return true;
|
|
398
|
+
}
|
|
399
|
+
this.#debug("Disabling microphone...");
|
|
351
400
|
this.#localStream.getTracks().forEach((track) => {
|
|
352
401
|
track.stop();
|
|
353
402
|
// Remove from PC if needed, or just stop sending
|
|
@@ -361,6 +410,7 @@ export class WebRtcManager {
|
|
|
361
410
|
});
|
|
362
411
|
this.#localStream = null;
|
|
363
412
|
this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, null);
|
|
413
|
+
this.#debug("Microphone disabled");
|
|
364
414
|
return true;
|
|
365
415
|
}
|
|
366
416
|
}
|
|
@@ -369,6 +419,7 @@ export class WebRtcManager {
|
|
|
369
419
|
* Transitions to DISCONNECTED state.
|
|
370
420
|
*/
|
|
371
421
|
disconnect() {
|
|
422
|
+
this.#debug("disconnect() called");
|
|
372
423
|
this.#cleanup();
|
|
373
424
|
this.#dispatch(WebRtcFsmEvent.DISCONNECT);
|
|
374
425
|
}
|
|
@@ -377,6 +428,7 @@ export class WebRtcManager {
|
|
|
377
428
|
* Cleans up all resources and allows reinitialization.
|
|
378
429
|
*/
|
|
379
430
|
reset() {
|
|
431
|
+
this.#debug("reset() called, current state:", this.state);
|
|
380
432
|
this.#cleanup();
|
|
381
433
|
// Reset from any non-IDLE state
|
|
382
434
|
if (this.state !== WebRtcState.IDLE) {
|
|
@@ -392,6 +444,7 @@ export class WebRtcManager {
|
|
|
392
444
|
this.#dispatch(WebRtcFsmEvent.RESET);
|
|
393
445
|
}
|
|
394
446
|
}
|
|
447
|
+
this.#debug("Reset complete, state:", this.state);
|
|
395
448
|
}
|
|
396
449
|
/**
|
|
397
450
|
* Creates a new data channel with the specified label.
|
|
@@ -401,14 +454,20 @@ export class WebRtcManager {
|
|
|
401
454
|
* @returns The created data channel, or null if peer connection not initialized.
|
|
402
455
|
*/
|
|
403
456
|
createDataChannel(label, options) {
|
|
404
|
-
|
|
457
|
+
this.#debug("createDataChannel() called:", label);
|
|
458
|
+
if (!this.#pc) {
|
|
459
|
+
this.#debug("Cannot create data channel: peer connection not initialized");
|
|
405
460
|
return null;
|
|
406
|
-
|
|
461
|
+
}
|
|
462
|
+
if (this.#dataChannels.has(label)) {
|
|
463
|
+
this.#debug("Returning existing data channel:", label);
|
|
407
464
|
return this.#dataChannels.get(label);
|
|
465
|
+
}
|
|
408
466
|
try {
|
|
409
467
|
const dc = this.#pc.createDataChannel(label, options);
|
|
410
468
|
this.#setupDataChannelListeners(dc);
|
|
411
469
|
this.#dataChannels.set(label, dc);
|
|
470
|
+
this.#debug("Data channel created:", label);
|
|
412
471
|
return dc;
|
|
413
472
|
}
|
|
414
473
|
catch (e) {
|
|
@@ -457,10 +516,14 @@ export class WebRtcManager {
|
|
|
457
516
|
* @returns The offer SDP, or null if peer connection not initialized.
|
|
458
517
|
*/
|
|
459
518
|
async createOffer(options) {
|
|
460
|
-
|
|
519
|
+
this.#debug("createOffer() called");
|
|
520
|
+
if (!this.#pc) {
|
|
521
|
+
this.#debug("Cannot create offer: peer connection not initialized");
|
|
461
522
|
return null;
|
|
523
|
+
}
|
|
462
524
|
try {
|
|
463
525
|
const offer = await this.#pc.createOffer(options);
|
|
526
|
+
this.#debug("Offer created:", offer.type);
|
|
464
527
|
return offer;
|
|
465
528
|
}
|
|
466
529
|
catch (e) {
|
|
@@ -474,10 +537,14 @@ export class WebRtcManager {
|
|
|
474
537
|
* @returns The answer SDP, or null if peer connection not initialized.
|
|
475
538
|
*/
|
|
476
539
|
async createAnswer(options) {
|
|
477
|
-
|
|
540
|
+
this.#debug("createAnswer() called");
|
|
541
|
+
if (!this.#pc) {
|
|
542
|
+
this.#debug("Cannot create answer: peer connection not initialized");
|
|
478
543
|
return null;
|
|
544
|
+
}
|
|
479
545
|
try {
|
|
480
546
|
const answer = await this.#pc.createAnswer(options);
|
|
547
|
+
this.#debug("Answer created:", answer.type);
|
|
481
548
|
return answer;
|
|
482
549
|
}
|
|
483
550
|
catch (e) {
|
|
@@ -491,10 +558,14 @@ export class WebRtcManager {
|
|
|
491
558
|
* @returns True if successful, false otherwise.
|
|
492
559
|
*/
|
|
493
560
|
async setLocalDescription(description) {
|
|
494
|
-
|
|
561
|
+
this.#debug("setLocalDescription() called:", description.type);
|
|
562
|
+
if (!this.#pc) {
|
|
563
|
+
this.#debug("Cannot set local description: peer connection not initialized");
|
|
495
564
|
return false;
|
|
565
|
+
}
|
|
496
566
|
try {
|
|
497
567
|
await this.#pc.setLocalDescription(description);
|
|
568
|
+
this.#debug("Local description set successfully");
|
|
498
569
|
return true;
|
|
499
570
|
}
|
|
500
571
|
catch (e) {
|
|
@@ -508,10 +579,14 @@ export class WebRtcManager {
|
|
|
508
579
|
* @returns True if successful, false otherwise.
|
|
509
580
|
*/
|
|
510
581
|
async setRemoteDescription(description) {
|
|
511
|
-
|
|
582
|
+
this.#debug("setRemoteDescription() called:", description.type);
|
|
583
|
+
if (!this.#pc) {
|
|
584
|
+
this.#debug("Cannot set remote description: peer connection not initialized");
|
|
512
585
|
return false;
|
|
586
|
+
}
|
|
513
587
|
try {
|
|
514
588
|
await this.#pc.setRemoteDescription(description);
|
|
589
|
+
this.#debug("Remote description set successfully");
|
|
515
590
|
return true;
|
|
516
591
|
}
|
|
517
592
|
catch (e) {
|
|
@@ -525,11 +600,15 @@ export class WebRtcManager {
|
|
|
525
600
|
* @returns True if successful, false otherwise.
|
|
526
601
|
*/
|
|
527
602
|
async addIceCandidate(candidate) {
|
|
528
|
-
|
|
603
|
+
this.#debug("addIceCandidate() called:", candidate ? "candidate" : "null (end-of-candidates)");
|
|
604
|
+
if (!this.#pc) {
|
|
605
|
+
this.#debug("Cannot add ICE candidate: peer connection not initialized");
|
|
529
606
|
return false;
|
|
607
|
+
}
|
|
530
608
|
try {
|
|
531
609
|
if (candidate) {
|
|
532
610
|
await this.#pc.addIceCandidate(candidate);
|
|
611
|
+
this.#debug("ICE candidate added");
|
|
533
612
|
}
|
|
534
613
|
return true;
|
|
535
614
|
}
|
|
@@ -544,11 +623,15 @@ export class WebRtcManager {
|
|
|
544
623
|
* @returns True if successful, false otherwise.
|
|
545
624
|
*/
|
|
546
625
|
async iceRestart() {
|
|
547
|
-
|
|
626
|
+
this.#debug("iceRestart() called");
|
|
627
|
+
if (!this.#pc) {
|
|
628
|
+
this.#debug("Cannot perform ICE restart: peer connection not initialized");
|
|
548
629
|
return false;
|
|
630
|
+
}
|
|
549
631
|
try {
|
|
550
632
|
const offer = await this.#pc.createOffer({ iceRestart: true });
|
|
551
633
|
await this.#pc.setLocalDescription(offer);
|
|
634
|
+
this.#debug("ICE restart initiated");
|
|
552
635
|
return true;
|
|
553
636
|
}
|
|
554
637
|
catch (e) {
|
|
@@ -591,24 +674,27 @@ export class WebRtcManager {
|
|
|
591
674
|
this.#fsm.transition(event);
|
|
592
675
|
const newState = this.#fsm.state;
|
|
593
676
|
if (oldState !== newState) {
|
|
677
|
+
this.#debug("State transition:", oldState, "->", newState, "(event:", event + ")");
|
|
594
678
|
this.#pubsub.publish(WebRtcManager.EVENT_STATE_CHANGE, newState);
|
|
595
679
|
}
|
|
596
680
|
}
|
|
597
681
|
#debug(...args) {
|
|
598
682
|
if (this.#config.debug) {
|
|
599
|
-
|
|
683
|
+
this.#logger.debug("[WebRtcManager]", ...args);
|
|
600
684
|
}
|
|
601
685
|
}
|
|
602
686
|
#error(error) {
|
|
603
|
-
|
|
687
|
+
this.#logger.error("[WebRtcManager]", error);
|
|
604
688
|
this.#dispatch(WebRtcFsmEvent.ERROR);
|
|
605
689
|
this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
|
|
606
690
|
}
|
|
607
691
|
#setupPcListeners() {
|
|
608
692
|
if (!this.#pc)
|
|
609
693
|
return;
|
|
694
|
+
this.#debug("Setting up peer connection listeners");
|
|
610
695
|
this.#pc.onconnectionstatechange = () => {
|
|
611
696
|
const state = this.#pc.connectionState;
|
|
697
|
+
this.#debug("Connection state changed:", state);
|
|
612
698
|
if (state === "connected") {
|
|
613
699
|
// Connection successful - reset reconnect attempts
|
|
614
700
|
this.#reconnectAttempts = 0;
|
|
@@ -623,6 +709,7 @@ export class WebRtcManager {
|
|
|
623
709
|
}
|
|
624
710
|
};
|
|
625
711
|
this.#pc.ontrack = (event) => {
|
|
712
|
+
this.#debug("Remote track received:", event.track.kind);
|
|
626
713
|
if (event.streams && event.streams[0]) {
|
|
627
714
|
this.#remoteStream = event.streams[0];
|
|
628
715
|
this.#pubsub.publish(WebRtcManager.EVENT_REMOTE_STREAM, this.#remoteStream);
|
|
@@ -630,14 +717,17 @@ export class WebRtcManager {
|
|
|
630
717
|
};
|
|
631
718
|
this.#pc.ondatachannel = (event) => {
|
|
632
719
|
const dc = event.channel;
|
|
720
|
+
this.#debug("Remote data channel received:", dc.label);
|
|
633
721
|
this.#setupDataChannelListeners(dc);
|
|
634
722
|
this.#dataChannels.set(dc.label, dc);
|
|
635
723
|
};
|
|
636
724
|
this.#pc.onicecandidate = (event) => {
|
|
725
|
+
this.#debug("ICE candidate generated:", event.candidate ? "candidate" : "null (gathering complete)");
|
|
637
726
|
this.#pubsub.publish(WebRtcManager.EVENT_ICE_CANDIDATE, event.candidate);
|
|
638
727
|
};
|
|
639
728
|
}
|
|
640
729
|
#cleanup() {
|
|
730
|
+
this.#debug("Cleanup started");
|
|
641
731
|
// Clear any pending reconnect timers
|
|
642
732
|
if (this.#reconnectTimer !== null) {
|
|
643
733
|
clearTimeout(this.#reconnectTimer);
|
|
@@ -649,33 +739,43 @@ export class WebRtcManager {
|
|
|
649
739
|
this.#deviceChangeHandler = null;
|
|
650
740
|
}
|
|
651
741
|
// Close all data channels
|
|
742
|
+
const dcCount = this.#dataChannels.size;
|
|
652
743
|
this.#dataChannels.forEach((dc) => {
|
|
653
744
|
if (dc.readyState !== "closed") {
|
|
654
745
|
dc.close();
|
|
655
746
|
}
|
|
656
747
|
});
|
|
657
748
|
this.#dataChannels.clear();
|
|
749
|
+
if (dcCount > 0) {
|
|
750
|
+
this.#debug("Closed", dcCount, "data channel(s)");
|
|
751
|
+
}
|
|
658
752
|
// Stop local stream tracks
|
|
659
753
|
if (this.#localStream) {
|
|
660
754
|
this.#localStream.getTracks().forEach((track) => track.stop());
|
|
661
755
|
this.#localStream = null;
|
|
756
|
+
this.#debug("Local stream stopped");
|
|
662
757
|
}
|
|
663
758
|
// Close peer connection
|
|
664
759
|
if (this.#pc) {
|
|
665
760
|
this.#pc.close();
|
|
666
761
|
this.#pc = null;
|
|
762
|
+
this.#debug("Peer connection closed");
|
|
667
763
|
}
|
|
668
764
|
this.#remoteStream = null;
|
|
765
|
+
this.#debug("Cleanup complete");
|
|
669
766
|
}
|
|
670
767
|
#handleConnectionFailure() {
|
|
768
|
+
this.#debug("Handling connection failure");
|
|
671
769
|
this.#dispatch(WebRtcFsmEvent.DISCONNECT);
|
|
672
770
|
// Check if auto-reconnect is enabled
|
|
673
771
|
if (!this.#config.autoReconnect) {
|
|
772
|
+
this.#debug("Auto-reconnect disabled, not attempting reconnection");
|
|
674
773
|
return;
|
|
675
774
|
}
|
|
676
775
|
const maxAttempts = this.#config.maxReconnectAttempts ?? 5;
|
|
677
776
|
// Check if we've exceeded max attempts
|
|
678
777
|
if (this.#reconnectAttempts >= maxAttempts) {
|
|
778
|
+
this.#debug("Max reconnection attempts reached:", maxAttempts);
|
|
679
779
|
this.#pubsub.publish(WebRtcManager.EVENT_RECONNECT_FAILED, {
|
|
680
780
|
attempts: this.#reconnectAttempts,
|
|
681
781
|
});
|
|
@@ -692,6 +792,11 @@ export class WebRtcManager {
|
|
|
692
792
|
const delay = baseDelay * Math.pow(2, this.#reconnectAttempts - 1);
|
|
693
793
|
// Try ICE restart first (attempts 1-2), then full reconnect
|
|
694
794
|
const strategy = this.#reconnectAttempts <= 2 ? "ice-restart" : "full";
|
|
795
|
+
this.#debug("Attempting reconnection:", {
|
|
796
|
+
attempt: this.#reconnectAttempts,
|
|
797
|
+
strategy,
|
|
798
|
+
delay: delay + "ms",
|
|
799
|
+
});
|
|
695
800
|
this.#pubsub.publish(WebRtcManager.EVENT_RECONNECTING, {
|
|
696
801
|
attempt: this.#reconnectAttempts,
|
|
697
802
|
strategy,
|
|
@@ -718,7 +823,7 @@ export class WebRtcManager {
|
|
|
718
823
|
// If successful, onconnectionstatechange will reset attempts
|
|
719
824
|
}
|
|
720
825
|
catch (e) {
|
|
721
|
-
|
|
826
|
+
this.#logger.error("[WebRtcManager] Reconnection failed:", e);
|
|
722
827
|
this.#handleConnectionFailure();
|
|
723
828
|
}
|
|
724
829
|
}
|
|
@@ -739,7 +844,7 @@ export class WebRtcManager {
|
|
|
739
844
|
this.#pubsub.publish(WebRtcManager.EVENT_DEVICE_CHANGED, devices);
|
|
740
845
|
}
|
|
741
846
|
catch (e) {
|
|
742
|
-
|
|
847
|
+
this.#logger.error("[WebRtcManager] Error handling device change:", e);
|
|
743
848
|
}
|
|
744
849
|
};
|
|
745
850
|
navigator.mediaDevices.addEventListener("devicechange", this.#deviceChangeHandler);
|
|
@@ -762,7 +867,7 @@ export class WebRtcManager {
|
|
|
762
867
|
// Ignore "User-Initiated Abort" errors which occur during intentional close()
|
|
763
868
|
const isUserAbort = error?.error?.message?.includes("User-Initiated Abort");
|
|
764
869
|
if (!isUserAbort) {
|
|
765
|
-
|
|
870
|
+
this.#logger.error("[WebRtcManager] Data channel error:", error);
|
|
766
871
|
this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
|
|
767
872
|
}
|
|
768
873
|
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/webrtc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/mod.js",
|
|
6
6
|
"types": "dist/mod.d.ts",
|
|
7
7
|
"author": "Marian Meres",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@marianmeres/fsm": "^2.
|
|
11
|
-
"@marianmeres/pubsub": "^2.4.
|
|
10
|
+
"@marianmeres/fsm": "^2.6.4",
|
|
11
|
+
"@marianmeres/pubsub": "^2.4.3"
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
package/llm.txt
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
# @marianmeres/webrtc - LLM Knowledge Base
|
|
2
|
-
|
|
3
|
-
## Package Identity
|
|
4
|
-
|
|
5
|
-
name: @marianmeres/webrtc
|
|
6
|
-
version: 0.0.2
|
|
7
|
-
license: MIT
|
|
8
|
-
author: Marian Meres
|
|
9
|
-
repository: https://github.com/marianmeres/webrtc
|
|
10
|
-
runtime: Deno (source), Node.js/Browser (distribution)
|
|
11
|
-
type: WebRTC connection management library
|
|
12
|
-
|
|
13
|
-
## Purpose
|
|
14
|
-
|
|
15
|
-
A lightweight, framework-agnostic WebRTC manager providing:
|
|
16
|
-
- Finite State Machine (FSM) based lifecycle management
|
|
17
|
-
- Event-driven architecture with PubSub pattern
|
|
18
|
-
- Svelte store compatibility
|
|
19
|
-
- Audio device management (microphone switching)
|
|
20
|
-
- Data channel support
|
|
21
|
-
- Auto-reconnection with exponential backoff
|
|
22
|
-
- Full TypeScript type safety
|
|
23
|
-
|
|
24
|
-
## Architecture Overview
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
WebRtcManager
|
|
28
|
-
├── FSM (@marianmeres/fsm) - State transitions
|
|
29
|
-
├── PubSub (@marianmeres/pubsub) - Event subscriptions
|
|
30
|
-
├── RTCPeerConnection - WebRTC connection (via factory)
|
|
31
|
-
├── MediaStream (local/remote) - Audio streams
|
|
32
|
-
└── DataChannels Map - RTCDataChannel instances
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Dependencies
|
|
36
|
-
|
|
37
|
-
Production:
|
|
38
|
-
- @marianmeres/fsm: ^2.3.0 (state machine)
|
|
39
|
-
- @marianmeres/pubsub: ^2.3.0 (event system)
|
|
40
|
-
|
|
41
|
-
Development (Deno):
|
|
42
|
-
- @std/assert: testing
|
|
43
|
-
- @std/fs: file operations
|
|
44
|
-
- @std/path: path utilities
|
|
45
|
-
|
|
46
|
-
## File Structure
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
src/
|
|
50
|
-
├── mod.ts # Entry point, re-exports all public APIs
|
|
51
|
-
├── types.ts # Type definitions (interfaces, enums)
|
|
52
|
-
└── webrtc-manager.ts # Main WebRtcManager class (839 lines)
|
|
53
|
-
|
|
54
|
-
tests/
|
|
55
|
-
├── mocks.ts # Mock WebRtcFactory for testing
|
|
56
|
-
├── webrtc-manager.test.ts # Deno unit tests
|
|
57
|
-
└── browser/
|
|
58
|
-
├── p2p-tests.ts # Browser integration tests
|
|
59
|
-
└── README.md # Browser test documentation
|
|
60
|
-
|
|
61
|
-
example/
|
|
62
|
-
├── peer.ts # Two-peer example with localStorage signaling
|
|
63
|
-
├── p2p.ts # Single-page P2P example
|
|
64
|
-
├── audio-peer.ts # Audio testing implementation
|
|
65
|
-
└── main.ts # Signaling server example
|
|
66
|
-
|
|
67
|
-
scripts/
|
|
68
|
-
├── build-npm.ts # npm distribution build
|
|
69
|
-
├── build-example.ts # Example bundling
|
|
70
|
-
├── build-browser-tests.ts
|
|
71
|
-
├── serve-browser-tests.ts
|
|
72
|
-
└── signaling-server.ts
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## State Machine
|
|
76
|
-
|
|
77
|
-
### States (WebRtcState enum)
|
|
78
|
-
|
|
79
|
-
| State | Description | Valid Transitions |
|
|
80
|
-
|-------|-------------|-------------------|
|
|
81
|
-
| IDLE | Initial state, no resources | → INITIALIZING |
|
|
82
|
-
| INITIALIZING | Creating peer connection | → CONNECTING, ERROR |
|
|
83
|
-
| CONNECTING | Performing SDP exchange | → CONNECTED, DISCONNECTED, ERROR |
|
|
84
|
-
| CONNECTED | Connection established | → DISCONNECTED, ERROR |
|
|
85
|
-
| RECONNECTING | Auto-reconnection in progress | → CONNECTING, DISCONNECTED, IDLE |
|
|
86
|
-
| DISCONNECTED | Closed but recoverable | → CONNECTING, RECONNECTING, IDLE |
|
|
87
|
-
| ERROR | Error occurred, must reset | → IDLE |
|
|
88
|
-
|
|
89
|
-
### Events (WebRtcFsmEvent enum)
|
|
90
|
-
|
|
91
|
-
| Event | Description |
|
|
92
|
-
|-------|-------------|
|
|
93
|
-
| INIT ("initialize") | Start initialization |
|
|
94
|
-
| CONNECT ("connect") | Begin connection |
|
|
95
|
-
| CONNECTED ("connected") | Connection succeeded |
|
|
96
|
-
| RECONNECTING ("reconnecting") | Start reconnection |
|
|
97
|
-
| DISCONNECT ("disconnect") | Close connection |
|
|
98
|
-
| ERROR ("error") | Error occurred |
|
|
99
|
-
| RESET ("reset") | Return to IDLE |
|
|
100
|
-
|
|
101
|
-
### State Transition Diagram
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
IDLE --INIT--> INITIALIZING --CONNECT--> CONNECTING --CONNECTED--> CONNECTED
|
|
105
|
-
| | |
|
|
106
|
-
v v v
|
|
107
|
-
ERROR <-----------------ERROR<----------------------ERROR
|
|
108
|
-
|
|
|
109
|
-
v
|
|
110
|
-
IDLE (via RESET)
|
|
111
|
-
|
|
112
|
-
CONNECTED --DISCONNECT--> DISCONNECTED --RESET--> IDLE
|
|
113
|
-
|
|
|
114
|
-
v
|
|
115
|
-
RECONNECTING --CONNECT--> CONNECTING
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## Public API
|
|
119
|
-
|
|
120
|
-
### Constructor
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
new WebRtcManager(factory: WebRtcFactory, config?: WebRtcManagerConfig)
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### WebRtcFactory Interface
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
interface WebRtcFactory {
|
|
130
|
-
createPeerConnection(config?: RTCConfiguration): RTCPeerConnection;
|
|
131
|
-
getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
|
132
|
-
enumerateDevices(): Promise<MediaDeviceInfo[]>;
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Browser implementation:
|
|
137
|
-
```typescript
|
|
138
|
-
const factory = {
|
|
139
|
-
createPeerConnection: (config) => new RTCPeerConnection(config),
|
|
140
|
-
getUserMedia: (constraints) => navigator.mediaDevices.getUserMedia(constraints),
|
|
141
|
-
enumerateDevices: () => navigator.mediaDevices.enumerateDevices(),
|
|
142
|
-
};
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### WebRtcManagerConfig Interface
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
interface WebRtcManagerConfig {
|
|
149
|
-
peerConfig?: RTCConfiguration; // ICE servers, certificates
|
|
150
|
-
enableMicrophone?: boolean; // Enable mic on init (default: false)
|
|
151
|
-
dataChannelLabel?: string; // Auto-create data channel
|
|
152
|
-
autoReconnect?: boolean; // Enable auto-reconnect (default: false)
|
|
153
|
-
maxReconnectAttempts?: number; // Max attempts (default: 5)
|
|
154
|
-
reconnectDelay?: number; // Initial delay ms (default: 1000)
|
|
155
|
-
debug?: boolean; // Enable logging (default: false)
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Properties (Getters)
|
|
160
|
-
|
|
161
|
-
| Property | Type | Description |
|
|
162
|
-
|----------|------|-------------|
|
|
163
|
-
| state | WebRtcState | Current FSM state |
|
|
164
|
-
| localStream | MediaStream \| null | Local audio stream |
|
|
165
|
-
| remoteStream | MediaStream \| null | Remote audio stream |
|
|
166
|
-
| dataChannels | ReadonlyMap<string, RTCDataChannel> | Active data channels |
|
|
167
|
-
| peerConnection | RTCPeerConnection \| null | Underlying connection |
|
|
168
|
-
|
|
169
|
-
### Lifecycle Methods
|
|
170
|
-
|
|
171
|
-
| Method | Signature | Description |
|
|
172
|
-
|--------|-----------|-------------|
|
|
173
|
-
| initialize | `(): Promise<void>` | Create peer connection, setup tracks |
|
|
174
|
-
| connect | `(): Promise<void>` | Transition to CONNECTING (auto-initializes) |
|
|
175
|
-
| disconnect | `(): void` | Close connection, cleanup resources |
|
|
176
|
-
| reset | `(): void` | Reset to IDLE from any state |
|
|
177
|
-
|
|
178
|
-
### Audio Methods
|
|
179
|
-
|
|
180
|
-
| Method | Signature | Returns | Description |
|
|
181
|
-
|--------|-----------|---------|-------------|
|
|
182
|
-
| enableMicrophone | `(enable: boolean): Promise<boolean>` | success | Enable/disable microphone |
|
|
183
|
-
| switchMicrophone | `(deviceId: string): Promise<boolean>` | success | Switch audio input device |
|
|
184
|
-
| getAudioInputDevices | `(): Promise<MediaDeviceInfo[]>` | devices | List audio inputs |
|
|
185
|
-
|
|
186
|
-
### Data Channel Methods
|
|
187
|
-
|
|
188
|
-
| Method | Signature | Returns | Description |
|
|
189
|
-
|--------|-----------|---------|-------------|
|
|
190
|
-
| createDataChannel | `(label: string, options?: RTCDataChannelInit): RTCDataChannel \| null` | channel | Create/get data channel |
|
|
191
|
-
| getDataChannel | `(label: string): RTCDataChannel \| undefined` | channel | Get existing channel |
|
|
192
|
-
| sendData | `(label: string, data: string \| Blob \| ArrayBuffer \| ArrayBufferView): boolean` | success | Send through channel |
|
|
193
|
-
|
|
194
|
-
### Signaling Methods
|
|
195
|
-
|
|
196
|
-
| Method | Signature | Returns | Description |
|
|
197
|
-
|--------|-----------|---------|-------------|
|
|
198
|
-
| createOffer | `(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit \| null>` | offer | Create SDP offer |
|
|
199
|
-
| createAnswer | `(options?: RTCAnswerOptions): Promise<RTCSessionDescriptionInit \| null>` | answer | Create SDP answer |
|
|
200
|
-
| setLocalDescription | `(description: RTCSessionDescriptionInit): Promise<boolean>` | success | Set local SDP |
|
|
201
|
-
| setRemoteDescription | `(description: RTCSessionDescriptionInit): Promise<boolean>` | success | Set remote SDP |
|
|
202
|
-
| addIceCandidate | `(candidate: RTCIceCandidateInit \| null): Promise<boolean>` | success | Add ICE candidate |
|
|
203
|
-
| iceRestart | `(): Promise<boolean>` | success | Perform ICE restart |
|
|
204
|
-
| getLocalDescription | `(): RTCSessionDescription \| null` | description | Get local SDP |
|
|
205
|
-
| getRemoteDescription | `(): RTCSessionDescription \| null` | description | Get remote SDP |
|
|
206
|
-
| getStats | `(): Promise<RTCStatsReport \| null>` | stats | Get connection statistics |
|
|
207
|
-
|
|
208
|
-
### Event Subscription Methods
|
|
209
|
-
|
|
210
|
-
| Method | Signature | Returns | Description |
|
|
211
|
-
|--------|-----------|---------|-------------|
|
|
212
|
-
| on | `<K extends keyof WebRtcEvents>(event: K, handler: (data: WebRtcEvents[K]) => void): () => void` | unsubscribe | Subscribe to specific event |
|
|
213
|
-
| subscribe | `(handler: (state: OverallState) => void): () => void` | unsubscribe | Subscribe to overall state (Svelte compatible) |
|
|
214
|
-
|
|
215
|
-
### Utility Methods
|
|
216
|
-
|
|
217
|
-
| Method | Signature | Returns | Description |
|
|
218
|
-
|--------|-----------|---------|-------------|
|
|
219
|
-
| toMermaid | `(): string` | mermaid | Get FSM as Mermaid diagram |
|
|
220
|
-
|
|
221
|
-
## Events
|
|
222
|
-
|
|
223
|
-
### Event Constants (Static)
|
|
224
|
-
|
|
225
|
-
| Constant | Value | Payload Type |
|
|
226
|
-
|----------|-------|--------------|
|
|
227
|
-
| EVENT_STATE_CHANGE | "state_change" | WebRtcState |
|
|
228
|
-
| EVENT_LOCAL_STREAM | "local_stream" | MediaStream \| null |
|
|
229
|
-
| EVENT_REMOTE_STREAM | "remote_stream" | MediaStream \| null |
|
|
230
|
-
| EVENT_DATA_CHANNEL_OPEN | "data_channel_open" | RTCDataChannel |
|
|
231
|
-
| EVENT_DATA_CHANNEL_MESSAGE | "data_channel_message" | { channel: RTCDataChannel; data: any } |
|
|
232
|
-
| EVENT_DATA_CHANNEL_CLOSE | "data_channel_close" | RTCDataChannel |
|
|
233
|
-
| EVENT_ICE_CANDIDATE | "ice_candidate" | RTCIceCandidate \| null |
|
|
234
|
-
| EVENT_RECONNECTING | "reconnecting" | { attempt: number; strategy: "ice-restart" \| "full" } |
|
|
235
|
-
| EVENT_RECONNECT_FAILED | "reconnect_failed" | { attempts: number } |
|
|
236
|
-
| EVENT_DEVICE_CHANGED | "device_changed" | MediaDeviceInfo[] |
|
|
237
|
-
| EVENT_MICROPHONE_FAILED | "microphone_failed" | { error?: any; reason?: string } |
|
|
238
|
-
| EVENT_ERROR | "error" | Error |
|
|
239
|
-
|
|
240
|
-
### WebRtcEvents Interface
|
|
241
|
-
|
|
242
|
-
```typescript
|
|
243
|
-
interface WebRtcEvents {
|
|
244
|
-
state_change: WebRtcState;
|
|
245
|
-
local_stream: MediaStream | null;
|
|
246
|
-
remote_stream: MediaStream | null;
|
|
247
|
-
data_channel_open: RTCDataChannel;
|
|
248
|
-
data_channel_message: { channel: RTCDataChannel; data: any };
|
|
249
|
-
data_channel_close: RTCDataChannel;
|
|
250
|
-
ice_candidate: RTCIceCandidate | null;
|
|
251
|
-
reconnecting: { attempt: number; strategy: "ice-restart" | "full" };
|
|
252
|
-
reconnect_failed: { attempts: number };
|
|
253
|
-
device_changed: MediaDeviceInfo[];
|
|
254
|
-
microphone_failed: { error?: any; reason?: string };
|
|
255
|
-
error: Error;
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
## Signaling Flow (User Responsibility)
|
|
260
|
-
|
|
261
|
-
The library does NOT handle signaling transport. Users must:
|
|
262
|
-
|
|
263
|
-
1. Create signaling channel (WebSocket, HTTP, localStorage, etc.)
|
|
264
|
-
2. Listen for `ice_candidate` events and send to remote peer
|
|
265
|
-
3. Send offers/answers through signaling channel
|
|
266
|
-
4. Receive remote offers/answers and call setRemoteDescription
|
|
267
|
-
5. Receive remote ICE candidates and call addIceCandidate
|
|
268
|
-
|
|
269
|
-
### Offer/Answer Flow
|
|
270
|
-
|
|
271
|
-
```
|
|
272
|
-
Initiator: Responder:
|
|
273
|
-
1. initialize()
|
|
274
|
-
2. connect()
|
|
275
|
-
3. createOffer()
|
|
276
|
-
4. setLocalDescription(offer)
|
|
277
|
-
5. [send offer via signaling] ───→ 6. initialize()
|
|
278
|
-
7. setRemoteDescription(offer)
|
|
279
|
-
8. createAnswer()
|
|
280
|
-
9. setLocalDescription(answer)
|
|
281
|
-
10. setRemoteDescription(answer) ←── [send answer via signaling]
|
|
282
|
-
11. [exchange ICE candidates] ←───→ [exchange ICE candidates]
|
|
283
|
-
12. CONNECTED 12. CONNECTED
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## Reconnection Strategy
|
|
287
|
-
|
|
288
|
-
When autoReconnect is enabled:
|
|
289
|
-
|
|
290
|
-
1. Attempts 1-2: ICE restart (preserves connection, quick recovery)
|
|
291
|
-
2. Attempts 3+: Full reconnection (new peer connection)
|
|
292
|
-
3. Exponential backoff: delay * 2^(attempt-1)
|
|
293
|
-
4. Max attempts configurable (default: 5)
|
|
294
|
-
|
|
295
|
-
For "full" strategy reconnections, consumers MUST:
|
|
296
|
-
- Listen for `reconnecting` event with strategy="full"
|
|
297
|
-
- Re-perform signaling handshake (create new offer/answer)
|
|
298
|
-
|
|
299
|
-
## Error Handling Patterns
|
|
300
|
-
|
|
301
|
-
1. Methods return boolean for success/failure
|
|
302
|
-
2. Critical errors transition to ERROR state
|
|
303
|
-
3. ERROR state requires reset() to recover
|
|
304
|
-
4. EVENT_ERROR emitted for all errors
|
|
305
|
-
5. Specific events: EVENT_MICROPHONE_FAILED, EVENT_RECONNECT_FAILED
|
|
306
|
-
|
|
307
|
-
## Testing
|
|
308
|
-
|
|
309
|
-
### Unit Tests (Deno)
|
|
310
|
-
```bash
|
|
311
|
-
deno task test
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Browser Integration Tests
|
|
315
|
-
```bash
|
|
316
|
-
deno task test:browser
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
## Build Commands
|
|
320
|
-
|
|
321
|
-
```bash
|
|
322
|
-
deno task npm:build # Build npm distribution
|
|
323
|
-
deno task npm:publish # Build and publish to npm
|
|
324
|
-
deno task build:example # Build examples
|
|
325
|
-
deno task serve:example # Run signaling server
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
## Important Implementation Details
|
|
329
|
-
|
|
330
|
-
1. subscribe() is Svelte store compatible (immediate callback + updates)
|
|
331
|
-
2. Data channels auto-cleanup on close
|
|
332
|
-
3. Device change listener auto-setup on initialize
|
|
333
|
-
4. "User-Initiated Abort" errors from intentional close() are ignored
|
|
334
|
-
5. recvonly transceiver added when microphone disabled (ensures audio SDP)
|
|
335
|
-
6. Private fields use # (true private, not accessible externally)
|
|
336
|
-
|
|
337
|
-
## Common Usage Patterns
|
|
338
|
-
|
|
339
|
-
### Minimal P2P Setup
|
|
340
|
-
```typescript
|
|
341
|
-
const manager = new WebRtcManager(factory, { enableMicrophone: true });
|
|
342
|
-
await manager.initialize();
|
|
343
|
-
await manager.connect();
|
|
344
|
-
const offer = await manager.createOffer();
|
|
345
|
-
await manager.setLocalDescription(offer);
|
|
346
|
-
// Send offer, receive answer, exchange ICE candidates...
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
### With Data Channel
|
|
350
|
-
```typescript
|
|
351
|
-
const manager = new WebRtcManager(factory, { dataChannelLabel: "chat" });
|
|
352
|
-
manager.on("data_channel_message", ({ data }) => console.log(data));
|
|
353
|
-
// After connection...
|
|
354
|
-
manager.sendData("chat", "Hello!");
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
### Svelte Integration
|
|
358
|
-
```svelte
|
|
359
|
-
<script>
|
|
360
|
-
const manager = new WebRtcManager(factory, config);
|
|
361
|
-
// $manager reactive access to state
|
|
362
|
-
</script>
|
|
363
|
-
{$manager.state}
|
|
364
|
-
```
|