@marianmeres/webrtc 1.0.0 → 1.0.3

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  ```yaml
6
6
  name: "@marianmeres/webrtc"
7
- version: "0.0.2"
7
+ version: "1.0.2"
8
8
  license: MIT
9
9
  author: Marian Meres
10
10
  repository: https://github.com/marianmeres/webrtc
@@ -27,12 +27,12 @@ A lightweight, framework-agnostic WebRTC manager providing:
27
27
 
28
28
  ```yaml
29
29
  production:
30
- - "@marianmeres/fsm": "^2.3.0"
31
- - "@marianmeres/pubsub": "^2.4.0"
30
+ - "@marianmeres/fsm": "^2.11.0"
31
+ - "@marianmeres/pubsub": "^2.4.4"
32
32
  development:
33
- - "@std/assert": testing
34
- - "@std/fs": file operations
35
- - "@std/path": path utilities
33
+ - "@std/assert": "^1.0.16"
34
+ - "@std/fs": "^1.0.20"
35
+ - "@std/path": "^1.1.3"
36
36
  ```
37
37
 
38
38
  ## File Structure
@@ -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[]) => any;
127
+ log: (...args: any[]) => any;
128
+ warn: (...args: any[]) => any;
129
+ error: (...args: any[]) => any;
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[]) => any;
691
+ log: (...args: any[]) => any;
692
+ warn: (...args: any[]) => any;
693
+ error: (...args: any[]) => any;
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,18 @@
1
+ /**
2
+ * Console-compatible logger interface (see @marianmeres/clog ).
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[]) => any;
8
+ log: (...args: any[]) => any;
9
+ warn: (...args: any[]) => any;
10
+ error: (...args: any[]) => any;
11
+ }
12
+ /**
13
+ * Configuration options for WebRtcManager.
14
+ * All options are optional with sensible defaults.
15
+ */
1
16
  export interface WebRtcManagerConfig {
2
17
  /** Initial peer configuration (ICE servers, etc.) */
3
18
  peerConfig?: RTCConfiguration;
@@ -11,42 +26,95 @@ export interface WebRtcManagerConfig {
11
26
  maxReconnectAttempts?: number;
12
27
  /** Initial reconnection delay in ms. Doubles with each attempt. Defaults to 1000. */
13
28
  reconnectDelay?: number;
14
- /** Debug mode for logging */
29
+ /** Enable debug logging. Defaults to false. */
15
30
  debug?: boolean;
31
+ /** Custom logger instance. If not provided, falls back to console. */
32
+ logger?: Logger;
16
33
  }
34
+ /**
35
+ * Factory interface for creating WebRTC primitives.
36
+ * Allows dependency injection of browser APIs for testing and flexibility.
37
+ */
17
38
  export interface WebRtcFactory {
39
+ /**
40
+ * Creates a new RTCPeerConnection instance.
41
+ * @param config - Optional RTCConfiguration for ICE servers, certificates, etc.
42
+ * @returns A new RTCPeerConnection instance.
43
+ */
18
44
  createPeerConnection(config?: RTCConfiguration): RTCPeerConnection;
45
+ /**
46
+ * Requests access to user media (microphone/camera).
47
+ * @param constraints - Media constraints specifying audio/video requirements.
48
+ * @returns Promise resolving to a MediaStream.
49
+ */
19
50
  getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
51
+ /**
52
+ * Enumerates all available media input and output devices.
53
+ * @returns Promise resolving to an array of MediaDeviceInfo objects.
54
+ */
20
55
  enumerateDevices(): Promise<MediaDeviceInfo[]>;
21
56
  }
57
+ /**
58
+ * Possible states of the WebRTC connection lifecycle.
59
+ * The manager transitions between these states based on connection events.
60
+ */
22
61
  export declare enum WebRtcState {
62
+ /** Initial state, no resources allocated. */
23
63
  IDLE = "IDLE",
64
+ /** Creating peer connection and setting up media tracks. */
24
65
  INITIALIZING = "INITIALIZING",
66
+ /** Performing SDP offer/answer exchange. */
25
67
  CONNECTING = "CONNECTING",
68
+ /** Connection established, communication active. */
26
69
  CONNECTED = "CONNECTED",
70
+ /** Automatic reconnection in progress. */
27
71
  RECONNECTING = "RECONNECTING",
72
+ /** Connection closed, can be restarted. */
28
73
  DISCONNECTED = "DISCONNECTED",
74
+ /** Error state, requires reset() to recover. */
29
75
  ERROR = "ERROR"
30
76
  }
77
+ /**
78
+ * Internal FSM events that trigger state transitions.
79
+ * These events are dispatched internally by the manager methods.
80
+ */
31
81
  export declare enum WebRtcFsmEvent {
82
+ /** Triggers transition from IDLE to INITIALIZING. */
32
83
  INIT = "initialize",
84
+ /** Triggers transition to CONNECTING state. */
33
85
  CONNECT = "connect",
86
+ /** Signals successful connection establishment. */
34
87
  CONNECTED = "connected",
88
+ /** Triggers transition to RECONNECTING state. */
35
89
  RECONNECTING = "reconnecting",
90
+ /** Triggers transition to DISCONNECTED state. */
36
91
  DISCONNECT = "disconnect",
92
+ /** Triggers transition to ERROR state. */
37
93
  ERROR = "error",
94
+ /** Resets the manager to IDLE state. */
38
95
  RESET = "reset"
39
96
  }
97
+ /**
98
+ * Type definitions for all WebRTC manager events and their payloads.
99
+ * Use with the `on()` method to subscribe to specific events.
100
+ */
40
101
  export interface WebRtcEvents {
102
+ /** Emitted when connection state changes. Payload: the new WebRtcState. */
41
103
  state_change: WebRtcState;
104
+ /** Emitted when local media stream changes. Payload: MediaStream or null if disabled. */
42
105
  local_stream: MediaStream | null;
106
+ /** Emitted when remote media stream is received. Payload: MediaStream or null. */
43
107
  remote_stream: MediaStream | null;
108
+ /** Emitted when a data channel opens. Payload: the RTCDataChannel. */
44
109
  data_channel_open: RTCDataChannel;
110
+ /** Emitted when a data channel receives a message. Payload: channel and data. */
45
111
  data_channel_message: {
46
112
  channel: RTCDataChannel;
47
113
  data: any;
48
114
  };
115
+ /** Emitted when a data channel closes. Payload: the RTCDataChannel. */
49
116
  data_channel_close: RTCDataChannel;
117
+ /** Emitted when an ICE candidate is generated. Payload: RTCIceCandidate or null. */
50
118
  ice_candidate: RTCIceCandidate | null;
51
119
  /**
52
120
  * Emitted when reconnection is being attempted.
@@ -58,13 +126,17 @@ export interface WebRtcEvents {
58
126
  attempt: number;
59
127
  strategy: "ice-restart" | "full";
60
128
  };
129
+ /** Emitted when all reconnection attempts have failed. Payload: total attempts. */
61
130
  reconnect_failed: {
62
131
  attempts: number;
63
132
  };
133
+ /** Emitted when audio devices change. Payload: array of available audio inputs. */
64
134
  device_changed: MediaDeviceInfo[];
135
+ /** Emitted when microphone access or switching fails. */
65
136
  microphone_failed: {
66
137
  error?: any;
67
138
  reason?: string;
68
139
  };
140
+ /** Emitted when an error occurs. Payload: the Error object. */
69
141
  error: Error;
70
142
  }
package/dist/types.js CHANGED
@@ -1,20 +1,42 @@
1
+ /**
2
+ * Possible states of the WebRTC connection lifecycle.
3
+ * The manager transitions between these states based on connection events.
4
+ */
1
5
  export var WebRtcState;
2
6
  (function (WebRtcState) {
7
+ /** Initial state, no resources allocated. */
3
8
  WebRtcState["IDLE"] = "IDLE";
9
+ /** Creating peer connection and setting up media tracks. */
4
10
  WebRtcState["INITIALIZING"] = "INITIALIZING";
11
+ /** Performing SDP offer/answer exchange. */
5
12
  WebRtcState["CONNECTING"] = "CONNECTING";
13
+ /** Connection established, communication active. */
6
14
  WebRtcState["CONNECTED"] = "CONNECTED";
15
+ /** Automatic reconnection in progress. */
7
16
  WebRtcState["RECONNECTING"] = "RECONNECTING";
17
+ /** Connection closed, can be restarted. */
8
18
  WebRtcState["DISCONNECTED"] = "DISCONNECTED";
19
+ /** Error state, requires reset() to recover. */
9
20
  WebRtcState["ERROR"] = "ERROR";
10
21
  })(WebRtcState || (WebRtcState = {}));
22
+ /**
23
+ * Internal FSM events that trigger state transitions.
24
+ * These events are dispatched internally by the manager methods.
25
+ */
11
26
  export var WebRtcFsmEvent;
12
27
  (function (WebRtcFsmEvent) {
28
+ /** Triggers transition from IDLE to INITIALIZING. */
13
29
  WebRtcFsmEvent["INIT"] = "initialize";
30
+ /** Triggers transition to CONNECTING state. */
14
31
  WebRtcFsmEvent["CONNECT"] = "connect";
32
+ /** Signals successful connection establishment. */
15
33
  WebRtcFsmEvent["CONNECTED"] = "connected";
34
+ /** Triggers transition to RECONNECTING state. */
16
35
  WebRtcFsmEvent["RECONNECTING"] = "reconnecting";
36
+ /** Triggers transition to DISCONNECTED state. */
17
37
  WebRtcFsmEvent["DISCONNECT"] = "disconnect";
38
+ /** Triggers transition to ERROR state. */
18
39
  WebRtcFsmEvent["ERROR"] = "error";
40
+ /** Resets the manager to IDLE state. */
19
41
  WebRtcFsmEvent["RESET"] = "reset";
20
42
  })(WebRtcFsmEvent || (WebRtcFsmEvent = {}));
@@ -1,6 +1,32 @@
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.
6
+ * Returns string representation of the first argument for chaining.
7
+ */
8
+ const createDefaultLogger = () => ({
9
+ // deno-lint-ignore no-explicit-any
10
+ debug: (...args) => {
11
+ console.debug(...args);
12
+ return String(args[0] ?? "");
13
+ },
14
+ // deno-lint-ignore no-explicit-any
15
+ log: (...args) => {
16
+ console.log(...args);
17
+ return String(args[0] ?? "");
18
+ },
19
+ // deno-lint-ignore no-explicit-any
20
+ warn: (...args) => {
21
+ console.warn(...args);
22
+ return String(args[0] ?? "");
23
+ },
24
+ // deno-lint-ignore no-explicit-any
25
+ error: (...args) => {
26
+ console.error(...args);
27
+ return String(args[0] ?? "");
28
+ },
29
+ });
4
30
  /**
5
31
  * WebRTC connection manager with FSM-based lifecycle and event-driven architecture.
6
32
  *
@@ -57,6 +83,7 @@ export class WebRtcManager {
57
83
  #pc = null;
58
84
  #factory;
59
85
  #config;
86
+ #logger;
60
87
  #localStream = null;
61
88
  #remoteStream = null;
62
89
  #dataChannels = new Map();
@@ -71,6 +98,7 @@ export class WebRtcManager {
71
98
  constructor(factory, config = {}) {
72
99
  this.#factory = factory;
73
100
  this.#config = config;
101
+ this.#logger = config.logger ?? createDefaultLogger();
74
102
  this.#pubsub = new PubSub();
75
103
  // Initialize FSM
76
104
  this.#fsm = new FSM({
@@ -149,6 +177,7 @@ export class WebRtcManager {
149
177
  * @param handler - Callback function that receives the event data.
150
178
  * @returns Unsubscribe function to remove the event listener.
151
179
  */
180
+ // deno-lint-ignore no-explicit-any
152
181
  on(event, handler) {
153
182
  return this.#pubsub.subscribe(event, handler);
154
183
  }
@@ -193,7 +222,7 @@ export class WebRtcManager {
193
222
  return devices.filter((d) => d.kind === "audioinput");
194
223
  }
195
224
  catch (e) {
196
- console.error("Failed to enumerate devices:", e);
225
+ this.#logger.error("[WebRtcManager] Failed to enumerate devices:", e);
197
226
  return [];
198
227
  }
199
228
  }
@@ -204,7 +233,7 @@ export class WebRtcManager {
204
233
  */
205
234
  async switchMicrophone(deviceId) {
206
235
  if (!this.#pc || !this.#localStream) {
207
- console.error("Cannot switch microphone: not initialized or no active stream");
236
+ this.#logger.error("[WebRtcManager] Cannot switch microphone: not initialized or no active stream");
208
237
  return false;
209
238
  }
210
239
  try {
@@ -240,7 +269,7 @@ export class WebRtcManager {
240
269
  return true;
241
270
  }
242
271
  catch (e) {
243
- console.error("Failed to switch microphone:", e);
272
+ this.#logger.error("[WebRtcManager] Failed to switch microphone:", e);
244
273
  this.#error(e);
245
274
  return false;
246
275
  }
@@ -250,15 +279,20 @@ export class WebRtcManager {
250
279
  * Must be called before creating offers or answers. Can only be called from IDLE state.
251
280
  */
252
281
  async initialize() {
253
- if (this.state !== WebRtcState.IDLE)
282
+ if (this.state !== WebRtcState.IDLE) {
283
+ this.#debug("initialize() called but state is not IDLE:", this.state);
254
284
  return;
285
+ }
286
+ this.#debug("Initializing...");
255
287
  this.#dispatch(WebRtcFsmEvent.INIT);
256
288
  try {
257
289
  this.#pc = this.#factory.createPeerConnection(this.#config.peerConfig);
290
+ this.#debug("Peer connection created");
258
291
  this.#setupPcListeners();
259
292
  // Setup device change detection now that we have a connection
260
293
  this.#setupDeviceChangeListener();
261
294
  if (this.#config.enableMicrophone) {
295
+ this.#debug("Enabling microphone (config enabled)");
262
296
  const success = await this.enableMicrophone(true);
263
297
  if (!success) {
264
298
  this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
@@ -270,10 +304,13 @@ export class WebRtcManager {
270
304
  // Always setup to receive audio, even if we don't enable microphone
271
305
  // This ensures the SDP includes audio media line
272
306
  this.#pc.addTransceiver("audio", { direction: "recvonly" });
307
+ this.#debug("Added recvonly audio transceiver");
273
308
  }
274
309
  if (this.#config.dataChannelLabel) {
310
+ this.#debug("Creating default data channel:", this.#config.dataChannelLabel);
275
311
  this.createDataChannel(this.#config.dataChannelLabel);
276
312
  }
313
+ this.#debug("Initialization complete");
277
314
  }
278
315
  catch (e) {
279
316
  this.#error(e);
@@ -284,12 +321,15 @@ export class WebRtcManager {
284
321
  * If disconnected, reinitializes the peer connection.
285
322
  */
286
323
  async connect() {
324
+ this.#debug("connect() called, current state:", this.state);
287
325
  // Initialize if needed
288
326
  if (this.state === WebRtcState.IDLE) {
327
+ this.#debug("State is IDLE, initializing first");
289
328
  await this.initialize();
290
329
  }
291
330
  // Reinitialize if disconnected (PeerConnection was closed)
292
331
  if (this.state === WebRtcState.DISCONNECTED) {
332
+ this.#debug("State is DISCONNECTED, reinitializing");
293
333
  // Clean up old connection
294
334
  this.#cleanup();
295
335
  // Reset to IDLE and reinitialize
@@ -299,8 +339,11 @@ export class WebRtcManager {
299
339
  return;
300
340
  }
301
341
  if (this.state === WebRtcState.CONNECTED ||
302
- this.state === WebRtcState.CONNECTING)
342
+ this.state === WebRtcState.CONNECTING) {
343
+ this.#debug("Already connected or connecting, skipping");
303
344
  return;
345
+ }
346
+ this.#debug("Transitioning to CONNECTING");
304
347
  this.#dispatch(WebRtcFsmEvent.CONNECT);
305
348
  }
306
349
  /**
@@ -309,14 +352,19 @@ export class WebRtcManager {
309
352
  * @returns True if successful, false if failed to get user media.
310
353
  */
311
354
  async enableMicrophone(enable) {
355
+ this.#debug("enableMicrophone() called:", enable);
312
356
  if (enable) {
313
- if (this.#localStream)
314
- return true; // Already enabled
357
+ if (this.#localStream) {
358
+ this.#debug("Microphone already enabled");
359
+ return true;
360
+ }
315
361
  try {
362
+ this.#debug("Requesting user media...");
316
363
  const stream = await this.#factory.getUserMedia({
317
364
  audio: true,
318
365
  video: false,
319
366
  });
367
+ this.#debug("User media obtained, tracks:", stream.getAudioTracks().length);
320
368
  this.#localStream = stream;
321
369
  this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, stream);
322
370
  if (this.#pc) {
@@ -329,25 +377,33 @@ export class WebRtcManager {
329
377
  await audioTransceiver.sender.replaceTrack(track);
330
378
  // Update direction to sendrecv
331
379
  audioTransceiver.direction = "sendrecv";
380
+ this.#debug("Replaced track in existing transceiver");
332
381
  }
333
382
  else {
334
383
  // Add track normally
335
384
  stream.getTracks().forEach((track) => {
336
385
  this.#pc.addTrack(track, stream);
337
386
  });
387
+ this.#debug("Added tracks to peer connection");
338
388
  }
339
389
  }
390
+ this.#debug("Microphone enabled successfully");
340
391
  return true;
341
392
  }
342
393
  catch (e) {
343
- console.error("Failed to get user media", e);
344
- this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, { error: e });
394
+ this.#logger.error("[WebRtcManager] Failed to get user media:", e);
395
+ this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
396
+ error: e,
397
+ });
345
398
  return false;
346
399
  }
347
400
  }
348
401
  else {
349
- if (!this.#localStream)
402
+ if (!this.#localStream) {
403
+ this.#debug("Microphone already disabled");
350
404
  return true;
405
+ }
406
+ this.#debug("Disabling microphone...");
351
407
  this.#localStream.getTracks().forEach((track) => {
352
408
  track.stop();
353
409
  // Remove from PC if needed, or just stop sending
@@ -361,6 +417,7 @@ export class WebRtcManager {
361
417
  });
362
418
  this.#localStream = null;
363
419
  this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, null);
420
+ this.#debug("Microphone disabled");
364
421
  return true;
365
422
  }
366
423
  }
@@ -369,6 +426,7 @@ export class WebRtcManager {
369
426
  * Transitions to DISCONNECTED state.
370
427
  */
371
428
  disconnect() {
429
+ this.#debug("disconnect() called");
372
430
  this.#cleanup();
373
431
  this.#dispatch(WebRtcFsmEvent.DISCONNECT);
374
432
  }
@@ -377,6 +435,7 @@ export class WebRtcManager {
377
435
  * Cleans up all resources and allows reinitialization.
378
436
  */
379
437
  reset() {
438
+ this.#debug("reset() called, current state:", this.state);
380
439
  this.#cleanup();
381
440
  // Reset from any non-IDLE state
382
441
  if (this.state !== WebRtcState.IDLE) {
@@ -392,6 +451,7 @@ export class WebRtcManager {
392
451
  this.#dispatch(WebRtcFsmEvent.RESET);
393
452
  }
394
453
  }
454
+ this.#debug("Reset complete, state:", this.state);
395
455
  }
396
456
  /**
397
457
  * Creates a new data channel with the specified label.
@@ -401,14 +461,20 @@ export class WebRtcManager {
401
461
  * @returns The created data channel, or null if peer connection not initialized.
402
462
  */
403
463
  createDataChannel(label, options) {
404
- if (!this.#pc)
464
+ this.#debug("createDataChannel() called:", label);
465
+ if (!this.#pc) {
466
+ this.#debug("Cannot create data channel: peer connection not initialized");
405
467
  return null;
406
- if (this.#dataChannels.has(label))
468
+ }
469
+ if (this.#dataChannels.has(label)) {
470
+ this.#debug("Returning existing data channel:", label);
407
471
  return this.#dataChannels.get(label);
472
+ }
408
473
  try {
409
474
  const dc = this.#pc.createDataChannel(label, options);
410
475
  this.#setupDataChannelListeners(dc);
411
476
  this.#dataChannels.set(label, dc);
477
+ this.#debug("Data channel created:", label);
412
478
  return dc;
413
479
  }
414
480
  catch (e) {
@@ -457,10 +523,14 @@ export class WebRtcManager {
457
523
  * @returns The offer SDP, or null if peer connection not initialized.
458
524
  */
459
525
  async createOffer(options) {
460
- if (!this.#pc)
526
+ this.#debug("createOffer() called");
527
+ if (!this.#pc) {
528
+ this.#debug("Cannot create offer: peer connection not initialized");
461
529
  return null;
530
+ }
462
531
  try {
463
532
  const offer = await this.#pc.createOffer(options);
533
+ this.#debug("Offer created:", offer.type);
464
534
  return offer;
465
535
  }
466
536
  catch (e) {
@@ -474,10 +544,14 @@ export class WebRtcManager {
474
544
  * @returns The answer SDP, or null if peer connection not initialized.
475
545
  */
476
546
  async createAnswer(options) {
477
- if (!this.#pc)
547
+ this.#debug("createAnswer() called");
548
+ if (!this.#pc) {
549
+ this.#debug("Cannot create answer: peer connection not initialized");
478
550
  return null;
551
+ }
479
552
  try {
480
553
  const answer = await this.#pc.createAnswer(options);
554
+ this.#debug("Answer created:", answer.type);
481
555
  return answer;
482
556
  }
483
557
  catch (e) {
@@ -491,10 +565,14 @@ export class WebRtcManager {
491
565
  * @returns True if successful, false otherwise.
492
566
  */
493
567
  async setLocalDescription(description) {
494
- if (!this.#pc)
568
+ this.#debug("setLocalDescription() called:", description.type);
569
+ if (!this.#pc) {
570
+ this.#debug("Cannot set local description: peer connection not initialized");
495
571
  return false;
572
+ }
496
573
  try {
497
574
  await this.#pc.setLocalDescription(description);
575
+ this.#debug("Local description set successfully");
498
576
  return true;
499
577
  }
500
578
  catch (e) {
@@ -508,10 +586,14 @@ export class WebRtcManager {
508
586
  * @returns True if successful, false otherwise.
509
587
  */
510
588
  async setRemoteDescription(description) {
511
- if (!this.#pc)
589
+ this.#debug("setRemoteDescription() called:", description.type);
590
+ if (!this.#pc) {
591
+ this.#debug("Cannot set remote description: peer connection not initialized");
512
592
  return false;
593
+ }
513
594
  try {
514
595
  await this.#pc.setRemoteDescription(description);
596
+ this.#debug("Remote description set successfully");
515
597
  return true;
516
598
  }
517
599
  catch (e) {
@@ -525,11 +607,15 @@ export class WebRtcManager {
525
607
  * @returns True if successful, false otherwise.
526
608
  */
527
609
  async addIceCandidate(candidate) {
528
- if (!this.#pc)
610
+ this.#debug("addIceCandidate() called:", candidate ? "candidate" : "null (end-of-candidates)");
611
+ if (!this.#pc) {
612
+ this.#debug("Cannot add ICE candidate: peer connection not initialized");
529
613
  return false;
614
+ }
530
615
  try {
531
616
  if (candidate) {
532
617
  await this.#pc.addIceCandidate(candidate);
618
+ this.#debug("ICE candidate added");
533
619
  }
534
620
  return true;
535
621
  }
@@ -544,11 +630,15 @@ export class WebRtcManager {
544
630
  * @returns True if successful, false otherwise.
545
631
  */
546
632
  async iceRestart() {
547
- if (!this.#pc)
633
+ this.#debug("iceRestart() called");
634
+ if (!this.#pc) {
635
+ this.#debug("Cannot perform ICE restart: peer connection not initialized");
548
636
  return false;
637
+ }
549
638
  try {
550
639
  const offer = await this.#pc.createOffer({ iceRestart: true });
551
640
  await this.#pc.setLocalDescription(offer);
641
+ this.#debug("ICE restart initiated");
552
642
  return true;
553
643
  }
554
644
  catch (e) {
@@ -591,24 +681,29 @@ export class WebRtcManager {
591
681
  this.#fsm.transition(event);
592
682
  const newState = this.#fsm.state;
593
683
  if (oldState !== newState) {
684
+ this.#debug("State transition:", oldState, "->", newState, "(event:", event + ")");
594
685
  this.#pubsub.publish(WebRtcManager.EVENT_STATE_CHANGE, newState);
595
686
  }
596
687
  }
688
+ // deno-lint-ignore no-explicit-any
597
689
  #debug(...args) {
598
690
  if (this.#config.debug) {
599
- console.debug("[WebRtcManager]", ...args);
691
+ this.#logger.debug("[WebRtcManager]", ...args);
600
692
  }
601
693
  }
694
+ // deno-lint-ignore no-explicit-any
602
695
  #error(error) {
603
- console.error(error);
696
+ this.#logger.error("[WebRtcManager]", error);
604
697
  this.#dispatch(WebRtcFsmEvent.ERROR);
605
698
  this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
606
699
  }
607
700
  #setupPcListeners() {
608
701
  if (!this.#pc)
609
702
  return;
703
+ this.#debug("Setting up peer connection listeners");
610
704
  this.#pc.onconnectionstatechange = () => {
611
705
  const state = this.#pc.connectionState;
706
+ this.#debug("Connection state changed:", state);
612
707
  if (state === "connected") {
613
708
  // Connection successful - reset reconnect attempts
614
709
  this.#reconnectAttempts = 0;
@@ -619,10 +714,16 @@ export class WebRtcManager {
619
714
  this.#handleConnectionFailure();
620
715
  }
621
716
  else if (state === "disconnected" || state === "closed") {
622
- this.#dispatch(WebRtcFsmEvent.DISCONNECT);
717
+ // Only dispatch if not already in a terminal state
718
+ if (this.state !== WebRtcState.DISCONNECTED &&
719
+ this.state !== WebRtcState.ERROR &&
720
+ this.state !== WebRtcState.IDLE) {
721
+ this.#dispatch(WebRtcFsmEvent.DISCONNECT);
722
+ }
623
723
  }
624
724
  };
625
725
  this.#pc.ontrack = (event) => {
726
+ this.#debug("Remote track received:", event.track.kind);
626
727
  if (event.streams && event.streams[0]) {
627
728
  this.#remoteStream = event.streams[0];
628
729
  this.#pubsub.publish(WebRtcManager.EVENT_REMOTE_STREAM, this.#remoteStream);
@@ -630,14 +731,17 @@ export class WebRtcManager {
630
731
  };
631
732
  this.#pc.ondatachannel = (event) => {
632
733
  const dc = event.channel;
734
+ this.#debug("Remote data channel received:", dc.label);
633
735
  this.#setupDataChannelListeners(dc);
634
736
  this.#dataChannels.set(dc.label, dc);
635
737
  };
636
738
  this.#pc.onicecandidate = (event) => {
739
+ this.#debug("ICE candidate generated:", event.candidate ? "candidate" : "null (gathering complete)");
637
740
  this.#pubsub.publish(WebRtcManager.EVENT_ICE_CANDIDATE, event.candidate);
638
741
  };
639
742
  }
640
743
  #cleanup() {
744
+ this.#debug("Cleanup started");
641
745
  // Clear any pending reconnect timers
642
746
  if (this.#reconnectTimer !== null) {
643
747
  clearTimeout(this.#reconnectTimer);
@@ -649,33 +753,48 @@ export class WebRtcManager {
649
753
  this.#deviceChangeHandler = null;
650
754
  }
651
755
  // Close all data channels
756
+ const dcCount = this.#dataChannels.size;
652
757
  this.#dataChannels.forEach((dc) => {
653
758
  if (dc.readyState !== "closed") {
654
759
  dc.close();
655
760
  }
656
761
  });
657
762
  this.#dataChannels.clear();
763
+ if (dcCount > 0) {
764
+ this.#debug("Closed", dcCount, "data channel(s)");
765
+ }
658
766
  // Stop local stream tracks
659
767
  if (this.#localStream) {
660
768
  this.#localStream.getTracks().forEach((track) => track.stop());
661
769
  this.#localStream = null;
770
+ this.#debug("Local stream stopped");
662
771
  }
663
772
  // Close peer connection
664
773
  if (this.#pc) {
665
774
  this.#pc.close();
666
775
  this.#pc = null;
776
+ this.#debug("Peer connection closed");
667
777
  }
668
778
  this.#remoteStream = null;
779
+ this.#debug("Cleanup complete");
669
780
  }
670
781
  #handleConnectionFailure() {
671
- this.#dispatch(WebRtcFsmEvent.DISCONNECT);
782
+ this.#debug("Handling connection failure");
783
+ // Only dispatch DISCONNECT if not already in a terminal state
784
+ if (this.state !== WebRtcState.DISCONNECTED &&
785
+ this.state !== WebRtcState.ERROR &&
786
+ this.state !== WebRtcState.IDLE) {
787
+ this.#dispatch(WebRtcFsmEvent.DISCONNECT);
788
+ }
672
789
  // Check if auto-reconnect is enabled
673
790
  if (!this.#config.autoReconnect) {
791
+ this.#debug("Auto-reconnect disabled, not attempting reconnection");
674
792
  return;
675
793
  }
676
794
  const maxAttempts = this.#config.maxReconnectAttempts ?? 5;
677
795
  // Check if we've exceeded max attempts
678
796
  if (this.#reconnectAttempts >= maxAttempts) {
797
+ this.#debug("Max reconnection attempts reached:", maxAttempts);
679
798
  this.#pubsub.publish(WebRtcManager.EVENT_RECONNECT_FAILED, {
680
799
  attempts: this.#reconnectAttempts,
681
800
  });
@@ -692,6 +811,11 @@ export class WebRtcManager {
692
811
  const delay = baseDelay * Math.pow(2, this.#reconnectAttempts - 1);
693
812
  // Try ICE restart first (attempts 1-2), then full reconnect
694
813
  const strategy = this.#reconnectAttempts <= 2 ? "ice-restart" : "full";
814
+ this.#debug("Attempting reconnection:", {
815
+ attempt: this.#reconnectAttempts,
816
+ strategy,
817
+ delay: delay + "ms",
818
+ });
695
819
  this.#pubsub.publish(WebRtcManager.EVENT_RECONNECTING, {
696
820
  attempt: this.#reconnectAttempts,
697
821
  strategy,
@@ -718,7 +842,7 @@ export class WebRtcManager {
718
842
  // If successful, onconnectionstatechange will reset attempts
719
843
  }
720
844
  catch (e) {
721
- console.error("Reconnection failed:", e);
845
+ this.#logger.error("[WebRtcManager] Reconnection failed:", e);
722
846
  this.#handleConnectionFailure();
723
847
  }
724
848
  }
@@ -739,7 +863,7 @@ export class WebRtcManager {
739
863
  this.#pubsub.publish(WebRtcManager.EVENT_DEVICE_CHANGED, devices);
740
864
  }
741
865
  catch (e) {
742
- console.error("Error handling device change:", e);
866
+ this.#logger.error("[WebRtcManager] Error handling device change:", e);
743
867
  }
744
868
  };
745
869
  navigator.mediaDevices.addEventListener("devicechange", this.#deviceChangeHandler);
@@ -758,11 +882,12 @@ export class WebRtcManager {
758
882
  this.#pubsub.publish(WebRtcManager.EVENT_DATA_CHANNEL_CLOSE, dc);
759
883
  this.#dataChannels.delete(dc.label);
760
884
  };
885
+ // deno-lint-ignore no-explicit-any
761
886
  dc.onerror = (error) => {
762
887
  // Ignore "User-Initiated Abort" errors which occur during intentional close()
763
888
  const isUserAbort = error?.error?.message?.includes("User-Initiated Abort");
764
889
  if (!isUserAbort) {
765
- console.error("Data Channel Error:", error);
890
+ this.#logger.error("[WebRtcManager] Data channel error:", error);
766
891
  this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
767
892
  }
768
893
  };
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "@marianmeres/webrtc",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "main": "dist/mod.js",
6
6
  "types": "dist/mod.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/mod.d.ts",
10
+ "import": "./dist/mod.js"
11
+ }
12
+ },
7
13
  "author": "Marian Meres",
8
14
  "license": "MIT",
9
15
  "dependencies": {
10
- "@marianmeres/fsm": "^2.5.4",
11
- "@marianmeres/pubsub": "^2.4.2"
16
+ "@marianmeres/fsm": "^2.11.0",
17
+ "@marianmeres/pubsub": "^2.4.4"
12
18
  },
13
19
  "repository": {
14
20
  "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
- ```