@marianmeres/webrtc 0.0.2 → 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/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @marianmeres/webrtc
2
2
 
3
- > **Full Disclosure:** This code was written by Claude (Anthropic's AI). The human (that is @marianmeres) just asked nicely, occasionally said "thanks", and went through about 47 iterations of "could you improve this", "what about that", and "make it more Svelte-friendly". To be fair, the prompt engineering was top-notch. So if you find bugs, we'll split the blame 50/50. If it works perfectly, Claude gets 95% of the credit and @marianmeres gets the remaining 5% for excellent taste in asking the right questions. 🤖
3
+ [![NPM Version](https://img.shields.io/npm/v/@marianmeres/webrtc)](https://www.npmjs.com/package/@marianmeres/webrtc)
4
+ [![JSR Version](https://jsr.io/badges/@marianmeres/webrtc)](https://jsr.io/@marianmeres/webrtc)
4
5
 
5
6
  A lightweight, framework-agnostic WebRTC manager with state machine-based lifecycle management and event-driven architecture.
6
7
 
@@ -51,6 +52,7 @@ const manager = new WebRtcManager(factory, config);
51
52
  - `maxReconnectAttempts`: Max reconnection attempts (default: 5)
52
53
  - `reconnectDelay`: Initial reconnection delay in ms (default: 1000)
53
54
  - `debug`: Enable debug logging (default: false)
55
+ - `logger`: Custom logger instance implementing `Logger` interface (default: console)
54
56
 
55
57
  ### State and Properties
56
58
 
@@ -366,6 +368,10 @@ await connection.createOffer();
366
368
  connection.sendMessage('Hello!');
367
369
  ```
368
370
 
371
+ ## API Reference
372
+
373
+ For complete API documentation, see [API.md](API.md).
374
+
369
375
  ## State Machine
370
376
 
371
377
  The manager uses a finite state machine with the following states:
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,18 +1,61 @@
1
1
  import { type WebRtcFactory, type WebRtcManagerConfig, WebRtcState, type WebRtcEvents } from "./types.js";
2
+ /**
3
+ * WebRTC connection manager with FSM-based lifecycle and event-driven architecture.
4
+ *
5
+ * Provides a high-level API for managing WebRTC peer connections, audio streams,
6
+ * and data channels. The manager uses a finite state machine to handle connection
7
+ * lifecycle and emits events for all state changes and important occurrences.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const factory = {
12
+ * createPeerConnection: (config) => new RTCPeerConnection(config),
13
+ * getUserMedia: (constraints) => navigator.mediaDevices.getUserMedia(constraints),
14
+ * enumerateDevices: () => navigator.mediaDevices.enumerateDevices(),
15
+ * };
16
+ *
17
+ * const manager = new WebRtcManager(factory, {
18
+ * peerConfig: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] },
19
+ * enableMicrophone: true,
20
+ * });
21
+ *
22
+ * await manager.initialize();
23
+ * await manager.connect();
24
+ * const offer = await manager.createOffer();
25
+ * await manager.setLocalDescription(offer);
26
+ * ```
27
+ */
2
28
  export declare class WebRtcManager {
3
29
  #private;
30
+ /** Event emitted when connection state changes. Payload: {@link WebRtcState} */
4
31
  static readonly EVENT_STATE_CHANGE = "state_change";
32
+ /** Event emitted when local media stream changes. Payload: `MediaStream | null` */
5
33
  static readonly EVENT_LOCAL_STREAM = "local_stream";
34
+ /** Event emitted when remote media stream is received. Payload: `MediaStream | null` */
6
35
  static readonly EVENT_REMOTE_STREAM = "remote_stream";
36
+ /** Event emitted when a data channel opens. Payload: `RTCDataChannel` */
7
37
  static readonly EVENT_DATA_CHANNEL_OPEN = "data_channel_open";
38
+ /** Event emitted when a data channel receives a message. Payload: `{ channel: RTCDataChannel; data: any }` */
8
39
  static readonly EVENT_DATA_CHANNEL_MESSAGE = "data_channel_message";
40
+ /** Event emitted when a data channel closes. Payload: `RTCDataChannel` */
9
41
  static readonly EVENT_DATA_CHANNEL_CLOSE = "data_channel_close";
42
+ /** Event emitted when an ICE candidate is generated. Payload: `RTCIceCandidate | null` */
10
43
  static readonly EVENT_ICE_CANDIDATE = "ice_candidate";
44
+ /** Event emitted when reconnection is attempted. Payload: `{ attempt: number; strategy: "ice-restart" | "full" }` */
11
45
  static readonly EVENT_RECONNECTING = "reconnecting";
46
+ /** Event emitted when all reconnection attempts fail. Payload: `{ attempts: number }` */
12
47
  static readonly EVENT_RECONNECT_FAILED = "reconnect_failed";
48
+ /** Event emitted when audio devices change. Payload: `MediaDeviceInfo[]` */
13
49
  static readonly EVENT_DEVICE_CHANGED = "device_changed";
50
+ /** Event emitted when microphone access fails. Payload: `{ error?: any; reason?: string }` */
14
51
  static readonly EVENT_MICROPHONE_FAILED = "microphone_failed";
52
+ /** Event emitted when an error occurs. Payload: `Error` */
15
53
  static readonly EVENT_ERROR = "error";
54
+ /**
55
+ * Creates a new WebRtcManager instance.
56
+ * @param factory - Factory object providing WebRTC primitives (peer connection, media, devices).
57
+ * @param config - Optional configuration for the manager.
58
+ */
16
59
  constructor(factory: WebRtcFactory, config?: WebRtcManagerConfig);
17
60
  /** Returns the current state of the WebRTC connection. */
18
61
  get state(): WebRtcState;
@@ -28,6 +71,8 @@ export declare class WebRtcManager {
28
71
  toMermaid(): string;
29
72
  /**
30
73
  * Subscribe to a specific WebRTC event.
74
+ * @param event - The event name to subscribe to (e.g., "state_change", "ice_candidate").
75
+ * @param handler - Callback function that receives the event data.
31
76
  * @returns Unsubscribe function to remove the event listener.
32
77
  */
33
78
  on(event: keyof WebRtcEvents, handler: (data: any) => void): () => void;
@@ -1,34 +1,100 @@
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
+ });
26
+ /**
27
+ * WebRTC connection manager with FSM-based lifecycle and event-driven architecture.
28
+ *
29
+ * Provides a high-level API for managing WebRTC peer connections, audio streams,
30
+ * and data channels. The manager uses a finite state machine to handle connection
31
+ * lifecycle and emits events for all state changes and important occurrences.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const factory = {
36
+ * createPeerConnection: (config) => new RTCPeerConnection(config),
37
+ * getUserMedia: (constraints) => navigator.mediaDevices.getUserMedia(constraints),
38
+ * enumerateDevices: () => navigator.mediaDevices.enumerateDevices(),
39
+ * };
40
+ *
41
+ * const manager = new WebRtcManager(factory, {
42
+ * peerConfig: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] },
43
+ * enableMicrophone: true,
44
+ * });
45
+ *
46
+ * await manager.initialize();
47
+ * await manager.connect();
48
+ * const offer = await manager.createOffer();
49
+ * await manager.setLocalDescription(offer);
50
+ * ```
51
+ */
4
52
  export class WebRtcManager {
5
- // Event name constants
53
+ /** Event emitted when connection state changes. Payload: {@link WebRtcState} */
6
54
  static EVENT_STATE_CHANGE = "state_change";
55
+ /** Event emitted when local media stream changes. Payload: `MediaStream | null` */
7
56
  static EVENT_LOCAL_STREAM = "local_stream";
57
+ /** Event emitted when remote media stream is received. Payload: `MediaStream | null` */
8
58
  static EVENT_REMOTE_STREAM = "remote_stream";
59
+ /** Event emitted when a data channel opens. Payload: `RTCDataChannel` */
9
60
  static EVENT_DATA_CHANNEL_OPEN = "data_channel_open";
61
+ /** Event emitted when a data channel receives a message. Payload: `{ channel: RTCDataChannel; data: any }` */
10
62
  static EVENT_DATA_CHANNEL_MESSAGE = "data_channel_message";
63
+ /** Event emitted when a data channel closes. Payload: `RTCDataChannel` */
11
64
  static EVENT_DATA_CHANNEL_CLOSE = "data_channel_close";
65
+ /** Event emitted when an ICE candidate is generated. Payload: `RTCIceCandidate | null` */
12
66
  static EVENT_ICE_CANDIDATE = "ice_candidate";
67
+ /** Event emitted when reconnection is attempted. Payload: `{ attempt: number; strategy: "ice-restart" | "full" }` */
13
68
  static EVENT_RECONNECTING = "reconnecting";
69
+ /** Event emitted when all reconnection attempts fail. Payload: `{ attempts: number }` */
14
70
  static EVENT_RECONNECT_FAILED = "reconnect_failed";
71
+ /** Event emitted when audio devices change. Payload: `MediaDeviceInfo[]` */
15
72
  static EVENT_DEVICE_CHANGED = "device_changed";
73
+ /** Event emitted when microphone access fails. Payload: `{ error?: any; reason?: string }` */
16
74
  static EVENT_MICROPHONE_FAILED = "microphone_failed";
75
+ /** Event emitted when an error occurs. Payload: `Error` */
17
76
  static EVENT_ERROR = "error";
18
77
  #fsm;
19
78
  #pubsub;
20
79
  #pc = null;
21
80
  #factory;
22
81
  #config;
82
+ #logger;
23
83
  #localStream = null;
24
84
  #remoteStream = null;
25
85
  #dataChannels = new Map();
26
86
  #reconnectAttempts = 0;
27
87
  #reconnectTimer = null;
28
88
  #deviceChangeHandler = null;
89
+ /**
90
+ * Creates a new WebRtcManager instance.
91
+ * @param factory - Factory object providing WebRTC primitives (peer connection, media, devices).
92
+ * @param config - Optional configuration for the manager.
93
+ */
29
94
  constructor(factory, config = {}) {
30
95
  this.#factory = factory;
31
96
  this.#config = config;
97
+ this.#logger = config.logger ?? createDefaultLogger();
32
98
  this.#pubsub = new PubSub();
33
99
  // Initialize FSM
34
100
  this.#fsm = new FSM({
@@ -103,6 +169,8 @@ export class WebRtcManager {
103
169
  }
104
170
  /**
105
171
  * Subscribe to a specific WebRTC event.
172
+ * @param event - The event name to subscribe to (e.g., "state_change", "ice_candidate").
173
+ * @param handler - Callback function that receives the event data.
106
174
  * @returns Unsubscribe function to remove the event listener.
107
175
  */
108
176
  on(event, handler) {
@@ -149,7 +217,7 @@ export class WebRtcManager {
149
217
  return devices.filter((d) => d.kind === "audioinput");
150
218
  }
151
219
  catch (e) {
152
- console.error("Failed to enumerate devices:", e);
220
+ this.#logger.error("[WebRtcManager] Failed to enumerate devices:", e);
153
221
  return [];
154
222
  }
155
223
  }
@@ -160,7 +228,7 @@ export class WebRtcManager {
160
228
  */
161
229
  async switchMicrophone(deviceId) {
162
230
  if (!this.#pc || !this.#localStream) {
163
- 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");
164
232
  return false;
165
233
  }
166
234
  try {
@@ -196,7 +264,7 @@ export class WebRtcManager {
196
264
  return true;
197
265
  }
198
266
  catch (e) {
199
- console.error("Failed to switch microphone:", e);
267
+ this.#logger.error("[WebRtcManager] Failed to switch microphone:", e);
200
268
  this.#error(e);
201
269
  return false;
202
270
  }
@@ -206,15 +274,20 @@ export class WebRtcManager {
206
274
  * Must be called before creating offers or answers. Can only be called from IDLE state.
207
275
  */
208
276
  async initialize() {
209
- if (this.state !== WebRtcState.IDLE)
277
+ if (this.state !== WebRtcState.IDLE) {
278
+ this.#debug("initialize() called but state is not IDLE:", this.state);
210
279
  return;
280
+ }
281
+ this.#debug("Initializing...");
211
282
  this.#dispatch(WebRtcFsmEvent.INIT);
212
283
  try {
213
284
  this.#pc = this.#factory.createPeerConnection(this.#config.peerConfig);
285
+ this.#debug("Peer connection created");
214
286
  this.#setupPcListeners();
215
287
  // Setup device change detection now that we have a connection
216
288
  this.#setupDeviceChangeListener();
217
289
  if (this.#config.enableMicrophone) {
290
+ this.#debug("Enabling microphone (config enabled)");
218
291
  const success = await this.enableMicrophone(true);
219
292
  if (!success) {
220
293
  this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
@@ -226,10 +299,13 @@ export class WebRtcManager {
226
299
  // Always setup to receive audio, even if we don't enable microphone
227
300
  // This ensures the SDP includes audio media line
228
301
  this.#pc.addTransceiver("audio", { direction: "recvonly" });
302
+ this.#debug("Added recvonly audio transceiver");
229
303
  }
230
304
  if (this.#config.dataChannelLabel) {
305
+ this.#debug("Creating default data channel:", this.#config.dataChannelLabel);
231
306
  this.createDataChannel(this.#config.dataChannelLabel);
232
307
  }
308
+ this.#debug("Initialization complete");
233
309
  }
234
310
  catch (e) {
235
311
  this.#error(e);
@@ -240,12 +316,15 @@ export class WebRtcManager {
240
316
  * If disconnected, reinitializes the peer connection.
241
317
  */
242
318
  async connect() {
319
+ this.#debug("connect() called, current state:", this.state);
243
320
  // Initialize if needed
244
321
  if (this.state === WebRtcState.IDLE) {
322
+ this.#debug("State is IDLE, initializing first");
245
323
  await this.initialize();
246
324
  }
247
325
  // Reinitialize if disconnected (PeerConnection was closed)
248
326
  if (this.state === WebRtcState.DISCONNECTED) {
327
+ this.#debug("State is DISCONNECTED, reinitializing");
249
328
  // Clean up old connection
250
329
  this.#cleanup();
251
330
  // Reset to IDLE and reinitialize
@@ -255,8 +334,11 @@ export class WebRtcManager {
255
334
  return;
256
335
  }
257
336
  if (this.state === WebRtcState.CONNECTED ||
258
- this.state === WebRtcState.CONNECTING)
337
+ this.state === WebRtcState.CONNECTING) {
338
+ this.#debug("Already connected or connecting, skipping");
259
339
  return;
340
+ }
341
+ this.#debug("Transitioning to CONNECTING");
260
342
  this.#dispatch(WebRtcFsmEvent.CONNECT);
261
343
  }
262
344
  /**
@@ -265,14 +347,19 @@ export class WebRtcManager {
265
347
  * @returns True if successful, false if failed to get user media.
266
348
  */
267
349
  async enableMicrophone(enable) {
350
+ this.#debug("enableMicrophone() called:", enable);
268
351
  if (enable) {
269
- if (this.#localStream)
270
- return true; // Already enabled
352
+ if (this.#localStream) {
353
+ this.#debug("Microphone already enabled");
354
+ return true;
355
+ }
271
356
  try {
357
+ this.#debug("Requesting user media...");
272
358
  const stream = await this.#factory.getUserMedia({
273
359
  audio: true,
274
360
  video: false,
275
361
  });
362
+ this.#debug("User media obtained, tracks:", stream.getAudioTracks().length);
276
363
  this.#localStream = stream;
277
364
  this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, stream);
278
365
  if (this.#pc) {
@@ -285,25 +372,31 @@ export class WebRtcManager {
285
372
  await audioTransceiver.sender.replaceTrack(track);
286
373
  // Update direction to sendrecv
287
374
  audioTransceiver.direction = "sendrecv";
375
+ this.#debug("Replaced track in existing transceiver");
288
376
  }
289
377
  else {
290
378
  // Add track normally
291
379
  stream.getTracks().forEach((track) => {
292
380
  this.#pc.addTrack(track, stream);
293
381
  });
382
+ this.#debug("Added tracks to peer connection");
294
383
  }
295
384
  }
385
+ this.#debug("Microphone enabled successfully");
296
386
  return true;
297
387
  }
298
388
  catch (e) {
299
- console.error("Failed to get user media", e);
389
+ this.#logger.error("[WebRtcManager] Failed to get user media:", e);
300
390
  this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, { error: e });
301
391
  return false;
302
392
  }
303
393
  }
304
394
  else {
305
- if (!this.#localStream)
395
+ if (!this.#localStream) {
396
+ this.#debug("Microphone already disabled");
306
397
  return true;
398
+ }
399
+ this.#debug("Disabling microphone...");
307
400
  this.#localStream.getTracks().forEach((track) => {
308
401
  track.stop();
309
402
  // Remove from PC if needed, or just stop sending
@@ -317,6 +410,7 @@ export class WebRtcManager {
317
410
  });
318
411
  this.#localStream = null;
319
412
  this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, null);
413
+ this.#debug("Microphone disabled");
320
414
  return true;
321
415
  }
322
416
  }
@@ -325,6 +419,7 @@ export class WebRtcManager {
325
419
  * Transitions to DISCONNECTED state.
326
420
  */
327
421
  disconnect() {
422
+ this.#debug("disconnect() called");
328
423
  this.#cleanup();
329
424
  this.#dispatch(WebRtcFsmEvent.DISCONNECT);
330
425
  }
@@ -333,6 +428,7 @@ export class WebRtcManager {
333
428
  * Cleans up all resources and allows reinitialization.
334
429
  */
335
430
  reset() {
431
+ this.#debug("reset() called, current state:", this.state);
336
432
  this.#cleanup();
337
433
  // Reset from any non-IDLE state
338
434
  if (this.state !== WebRtcState.IDLE) {
@@ -348,6 +444,7 @@ export class WebRtcManager {
348
444
  this.#dispatch(WebRtcFsmEvent.RESET);
349
445
  }
350
446
  }
447
+ this.#debug("Reset complete, state:", this.state);
351
448
  }
352
449
  /**
353
450
  * Creates a new data channel with the specified label.
@@ -357,14 +454,20 @@ export class WebRtcManager {
357
454
  * @returns The created data channel, or null if peer connection not initialized.
358
455
  */
359
456
  createDataChannel(label, options) {
360
- if (!this.#pc)
457
+ this.#debug("createDataChannel() called:", label);
458
+ if (!this.#pc) {
459
+ this.#debug("Cannot create data channel: peer connection not initialized");
361
460
  return null;
362
- if (this.#dataChannels.has(label))
461
+ }
462
+ if (this.#dataChannels.has(label)) {
463
+ this.#debug("Returning existing data channel:", label);
363
464
  return this.#dataChannels.get(label);
465
+ }
364
466
  try {
365
467
  const dc = this.#pc.createDataChannel(label, options);
366
468
  this.#setupDataChannelListeners(dc);
367
469
  this.#dataChannels.set(label, dc);
470
+ this.#debug("Data channel created:", label);
368
471
  return dc;
369
472
  }
370
473
  catch (e) {
@@ -413,10 +516,14 @@ export class WebRtcManager {
413
516
  * @returns The offer SDP, or null if peer connection not initialized.
414
517
  */
415
518
  async createOffer(options) {
416
- if (!this.#pc)
519
+ this.#debug("createOffer() called");
520
+ if (!this.#pc) {
521
+ this.#debug("Cannot create offer: peer connection not initialized");
417
522
  return null;
523
+ }
418
524
  try {
419
525
  const offer = await this.#pc.createOffer(options);
526
+ this.#debug("Offer created:", offer.type);
420
527
  return offer;
421
528
  }
422
529
  catch (e) {
@@ -430,10 +537,14 @@ export class WebRtcManager {
430
537
  * @returns The answer SDP, or null if peer connection not initialized.
431
538
  */
432
539
  async createAnswer(options) {
433
- if (!this.#pc)
540
+ this.#debug("createAnswer() called");
541
+ if (!this.#pc) {
542
+ this.#debug("Cannot create answer: peer connection not initialized");
434
543
  return null;
544
+ }
435
545
  try {
436
546
  const answer = await this.#pc.createAnswer(options);
547
+ this.#debug("Answer created:", answer.type);
437
548
  return answer;
438
549
  }
439
550
  catch (e) {
@@ -447,10 +558,14 @@ export class WebRtcManager {
447
558
  * @returns True if successful, false otherwise.
448
559
  */
449
560
  async setLocalDescription(description) {
450
- 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");
451
564
  return false;
565
+ }
452
566
  try {
453
567
  await this.#pc.setLocalDescription(description);
568
+ this.#debug("Local description set successfully");
454
569
  return true;
455
570
  }
456
571
  catch (e) {
@@ -464,10 +579,14 @@ export class WebRtcManager {
464
579
  * @returns True if successful, false otherwise.
465
580
  */
466
581
  async setRemoteDescription(description) {
467
- 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");
468
585
  return false;
586
+ }
469
587
  try {
470
588
  await this.#pc.setRemoteDescription(description);
589
+ this.#debug("Remote description set successfully");
471
590
  return true;
472
591
  }
473
592
  catch (e) {
@@ -481,11 +600,15 @@ export class WebRtcManager {
481
600
  * @returns True if successful, false otherwise.
482
601
  */
483
602
  async addIceCandidate(candidate) {
484
- 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");
485
606
  return false;
607
+ }
486
608
  try {
487
609
  if (candidate) {
488
610
  await this.#pc.addIceCandidate(candidate);
611
+ this.#debug("ICE candidate added");
489
612
  }
490
613
  return true;
491
614
  }
@@ -500,11 +623,15 @@ export class WebRtcManager {
500
623
  * @returns True if successful, false otherwise.
501
624
  */
502
625
  async iceRestart() {
503
- if (!this.#pc)
626
+ this.#debug("iceRestart() called");
627
+ if (!this.#pc) {
628
+ this.#debug("Cannot perform ICE restart: peer connection not initialized");
504
629
  return false;
630
+ }
505
631
  try {
506
632
  const offer = await this.#pc.createOffer({ iceRestart: true });
507
633
  await this.#pc.setLocalDescription(offer);
634
+ this.#debug("ICE restart initiated");
508
635
  return true;
509
636
  }
510
637
  catch (e) {
@@ -547,24 +674,27 @@ export class WebRtcManager {
547
674
  this.#fsm.transition(event);
548
675
  const newState = this.#fsm.state;
549
676
  if (oldState !== newState) {
677
+ this.#debug("State transition:", oldState, "->", newState, "(event:", event + ")");
550
678
  this.#pubsub.publish(WebRtcManager.EVENT_STATE_CHANGE, newState);
551
679
  }
552
680
  }
553
681
  #debug(...args) {
554
682
  if (this.#config.debug) {
555
- console.debug("[WebRtcManager]", ...args);
683
+ this.#logger.debug("[WebRtcManager]", ...args);
556
684
  }
557
685
  }
558
686
  #error(error) {
559
- console.error(error);
687
+ this.#logger.error("[WebRtcManager]", error);
560
688
  this.#dispatch(WebRtcFsmEvent.ERROR);
561
689
  this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
562
690
  }
563
691
  #setupPcListeners() {
564
692
  if (!this.#pc)
565
693
  return;
694
+ this.#debug("Setting up peer connection listeners");
566
695
  this.#pc.onconnectionstatechange = () => {
567
696
  const state = this.#pc.connectionState;
697
+ this.#debug("Connection state changed:", state);
568
698
  if (state === "connected") {
569
699
  // Connection successful - reset reconnect attempts
570
700
  this.#reconnectAttempts = 0;
@@ -579,6 +709,7 @@ export class WebRtcManager {
579
709
  }
580
710
  };
581
711
  this.#pc.ontrack = (event) => {
712
+ this.#debug("Remote track received:", event.track.kind);
582
713
  if (event.streams && event.streams[0]) {
583
714
  this.#remoteStream = event.streams[0];
584
715
  this.#pubsub.publish(WebRtcManager.EVENT_REMOTE_STREAM, this.#remoteStream);
@@ -586,14 +717,17 @@ export class WebRtcManager {
586
717
  };
587
718
  this.#pc.ondatachannel = (event) => {
588
719
  const dc = event.channel;
720
+ this.#debug("Remote data channel received:", dc.label);
589
721
  this.#setupDataChannelListeners(dc);
590
722
  this.#dataChannels.set(dc.label, dc);
591
723
  };
592
724
  this.#pc.onicecandidate = (event) => {
725
+ this.#debug("ICE candidate generated:", event.candidate ? "candidate" : "null (gathering complete)");
593
726
  this.#pubsub.publish(WebRtcManager.EVENT_ICE_CANDIDATE, event.candidate);
594
727
  };
595
728
  }
596
729
  #cleanup() {
730
+ this.#debug("Cleanup started");
597
731
  // Clear any pending reconnect timers
598
732
  if (this.#reconnectTimer !== null) {
599
733
  clearTimeout(this.#reconnectTimer);
@@ -605,33 +739,43 @@ export class WebRtcManager {
605
739
  this.#deviceChangeHandler = null;
606
740
  }
607
741
  // Close all data channels
742
+ const dcCount = this.#dataChannels.size;
608
743
  this.#dataChannels.forEach((dc) => {
609
744
  if (dc.readyState !== "closed") {
610
745
  dc.close();
611
746
  }
612
747
  });
613
748
  this.#dataChannels.clear();
749
+ if (dcCount > 0) {
750
+ this.#debug("Closed", dcCount, "data channel(s)");
751
+ }
614
752
  // Stop local stream tracks
615
753
  if (this.#localStream) {
616
754
  this.#localStream.getTracks().forEach((track) => track.stop());
617
755
  this.#localStream = null;
756
+ this.#debug("Local stream stopped");
618
757
  }
619
758
  // Close peer connection
620
759
  if (this.#pc) {
621
760
  this.#pc.close();
622
761
  this.#pc = null;
762
+ this.#debug("Peer connection closed");
623
763
  }
624
764
  this.#remoteStream = null;
765
+ this.#debug("Cleanup complete");
625
766
  }
626
767
  #handleConnectionFailure() {
768
+ this.#debug("Handling connection failure");
627
769
  this.#dispatch(WebRtcFsmEvent.DISCONNECT);
628
770
  // Check if auto-reconnect is enabled
629
771
  if (!this.#config.autoReconnect) {
772
+ this.#debug("Auto-reconnect disabled, not attempting reconnection");
630
773
  return;
631
774
  }
632
775
  const maxAttempts = this.#config.maxReconnectAttempts ?? 5;
633
776
  // Check if we've exceeded max attempts
634
777
  if (this.#reconnectAttempts >= maxAttempts) {
778
+ this.#debug("Max reconnection attempts reached:", maxAttempts);
635
779
  this.#pubsub.publish(WebRtcManager.EVENT_RECONNECT_FAILED, {
636
780
  attempts: this.#reconnectAttempts,
637
781
  });
@@ -648,6 +792,11 @@ export class WebRtcManager {
648
792
  const delay = baseDelay * Math.pow(2, this.#reconnectAttempts - 1);
649
793
  // Try ICE restart first (attempts 1-2), then full reconnect
650
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
+ });
651
800
  this.#pubsub.publish(WebRtcManager.EVENT_RECONNECTING, {
652
801
  attempt: this.#reconnectAttempts,
653
802
  strategy,
@@ -674,7 +823,7 @@ export class WebRtcManager {
674
823
  // If successful, onconnectionstatechange will reset attempts
675
824
  }
676
825
  catch (e) {
677
- console.error("Reconnection failed:", e);
826
+ this.#logger.error("[WebRtcManager] Reconnection failed:", e);
678
827
  this.#handleConnectionFailure();
679
828
  }
680
829
  }
@@ -695,7 +844,7 @@ export class WebRtcManager {
695
844
  this.#pubsub.publish(WebRtcManager.EVENT_DEVICE_CHANGED, devices);
696
845
  }
697
846
  catch (e) {
698
- console.error("Error handling device change:", e);
847
+ this.#logger.error("[WebRtcManager] Error handling device change:", e);
699
848
  }
700
849
  };
701
850
  navigator.mediaDevices.addEventListener("devicechange", this.#deviceChangeHandler);
@@ -718,7 +867,7 @@ export class WebRtcManager {
718
867
  // Ignore "User-Initiated Abort" errors which occur during intentional close()
719
868
  const isUserAbort = error?.error?.message?.includes("User-Initiated Abort");
720
869
  if (!isUserAbort) {
721
- console.error("Data Channel Error:", error);
870
+ this.#logger.error("[WebRtcManager] Data channel error:", error);
722
871
  this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
723
872
  }
724
873
  };