@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 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
- /** Debug mode for logging */
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;
@@ -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
- console.error("Failed to enumerate devices:", e);
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
- console.error("Cannot switch microphone: not initialized or no active stream");
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
- console.error("Failed to switch microphone:", e);
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
- return true; // Already enabled
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
- console.error("Failed to get user media", e);
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
- if (!this.#pc)
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
- if (this.#dataChannels.has(label))
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
- if (!this.#pc)
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
- if (!this.#pc)
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
- if (!this.#pc)
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
- if (!this.#pc)
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
- if (!this.#pc)
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
- if (!this.#pc)
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
- console.debug("[WebRtcManager]", ...args);
683
+ this.#logger.debug("[WebRtcManager]", ...args);
600
684
  }
601
685
  }
602
686
  #error(error) {
603
- console.error(error);
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
- console.error("Reconnection failed:", e);
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
- console.error("Error handling device change:", e);
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
- console.error("Data Channel Error:", error);
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.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.5.4",
11
- "@marianmeres/pubsub": "^2.4.2"
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
- ```