@marianmeres/webrtc 1.3.1 → 1.4.1

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.
@@ -1,32 +1,7 @@
1
1
  import { FSM } from "@marianmeres/fsm";
2
2
  import { PubSub } from "@marianmeres/pubsub";
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
- });
3
+ import { createClog, withNamespace } from "@marianmeres/clog";
4
+ import { WebRTCState, WebRTCFsmEvent, } from "./types.js";
30
5
  /**
31
6
  * WebRTC connection manager with FSM-based lifecycle and event-driven architecture.
32
7
  *
@@ -42,7 +17,7 @@ const createDefaultLogger = () => ({
42
17
  * enumerateDevices: () => navigator.mediaDevices.enumerateDevices(),
43
18
  * };
44
19
  *
45
- * const manager = new WebRtcManager(factory, {
20
+ * const manager = new WebRTCManager(factory, {
46
21
  * peerConfig: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] },
47
22
  * enableMicrophone: true,
48
23
  * });
@@ -53,8 +28,8 @@ const createDefaultLogger = () => ({
53
28
  * await manager.setLocalDescription(offer);
54
29
  * ```
55
30
  */
56
- export class WebRtcManager {
57
- /** Event emitted when connection state changes. Payload: {@link WebRtcState} */
31
+ export class WebRTCManager {
32
+ /** Event emitted when connection state changes. Payload: {@link WebRTCState} */
58
33
  static EVENT_STATE_CHANGE = "state_change";
59
34
  /** Event emitted when local media stream changes. Payload: `MediaStream | null` */
60
35
  static EVENT_LOCAL_STREAM = "local_stream";
@@ -96,12 +71,12 @@ export class WebRtcManager {
96
71
  * @example
97
72
  * ```typescript
98
73
  * // With type parameter for full type safety:
99
- * const manager = new WebRtcManager<{ audioStream: MediaStream; sessionId: string }>(factory);
74
+ * const manager = new WebRTCManager<{ audioStream: MediaStream; sessionId: string }>(factory);
100
75
  * manager.context = { audioStream: myStream, sessionId: '123' };
101
76
  * manager.context.audioStream; // typed as MediaStream
102
77
  *
103
78
  * // Without type parameter (backwards compatible):
104
- * const manager = new WebRtcManager(factory);
79
+ * const manager = new WebRTCManager(factory);
105
80
  * manager.context = { anything: 'goes' };
106
81
  * ```
107
82
  */
@@ -110,57 +85,57 @@ export class WebRtcManager {
110
85
  #fullReconnectTimeoutTimer = null;
111
86
  #deviceChangeHandler = null;
112
87
  /**
113
- * Creates a new WebRtcManager instance.
88
+ * Creates a new WebRTCManager instance.
114
89
  * @param factory - Factory object providing WebRTC primitives (peer connection, media, devices).
115
90
  * @param config - Optional configuration for the manager.
116
91
  */
117
92
  constructor(factory, config = {}) {
118
93
  this.#factory = factory;
119
94
  this.#config = config;
120
- this.#logger = config.logger ?? createDefaultLogger();
95
+ this.#logger = withNamespace(config.logger ?? createClog(), "WebRTCManager");
121
96
  this.#pubsub = new PubSub();
122
97
  // Initialize FSM
123
98
  this.#fsm = new FSM({
124
- initial: WebRtcState.IDLE,
99
+ initial: WebRTCState.IDLE,
125
100
  states: {
126
- [WebRtcState.IDLE]: {
127
- on: { [WebRtcFsmEvent.INIT]: WebRtcState.INITIALIZING },
101
+ [WebRTCState.IDLE]: {
102
+ on: { [WebRTCFsmEvent.INIT]: WebRTCState.INITIALIZING },
128
103
  },
129
- [WebRtcState.INITIALIZING]: {
104
+ [WebRTCState.INITIALIZING]: {
130
105
  on: {
131
- [WebRtcFsmEvent.CONNECT]: WebRtcState.CONNECTING,
132
- [WebRtcFsmEvent.ERROR]: WebRtcState.ERROR,
106
+ [WebRTCFsmEvent.CONNECT]: WebRTCState.CONNECTING,
107
+ [WebRTCFsmEvent.ERROR]: WebRTCState.ERROR,
133
108
  },
134
109
  },
135
- [WebRtcState.CONNECTING]: {
110
+ [WebRTCState.CONNECTING]: {
136
111
  on: {
137
- [WebRtcFsmEvent.CONNECTED]: WebRtcState.CONNECTED,
138
- [WebRtcFsmEvent.DISCONNECT]: WebRtcState.DISCONNECTED,
139
- [WebRtcFsmEvent.ERROR]: WebRtcState.ERROR,
112
+ [WebRTCFsmEvent.CONNECTED]: WebRTCState.CONNECTED,
113
+ [WebRTCFsmEvent.DISCONNECT]: WebRTCState.DISCONNECTED,
114
+ [WebRTCFsmEvent.ERROR]: WebRTCState.ERROR,
140
115
  },
141
116
  },
142
- [WebRtcState.CONNECTED]: {
117
+ [WebRTCState.CONNECTED]: {
143
118
  on: {
144
- [WebRtcFsmEvent.DISCONNECT]: WebRtcState.DISCONNECTED,
145
- [WebRtcFsmEvent.ERROR]: WebRtcState.ERROR,
119
+ [WebRTCFsmEvent.DISCONNECT]: WebRTCState.DISCONNECTED,
120
+ [WebRTCFsmEvent.ERROR]: WebRTCState.ERROR,
146
121
  },
147
122
  },
148
- [WebRtcState.RECONNECTING]: {
123
+ [WebRTCState.RECONNECTING]: {
149
124
  on: {
150
- [WebRtcFsmEvent.CONNECT]: WebRtcState.CONNECTING,
151
- [WebRtcFsmEvent.DISCONNECT]: WebRtcState.DISCONNECTED,
152
- [WebRtcFsmEvent.RESET]: WebRtcState.IDLE,
125
+ [WebRTCFsmEvent.CONNECT]: WebRTCState.CONNECTING,
126
+ [WebRTCFsmEvent.DISCONNECT]: WebRTCState.DISCONNECTED,
127
+ [WebRTCFsmEvent.RESET]: WebRTCState.IDLE,
153
128
  },
154
129
  },
155
- [WebRtcState.DISCONNECTED]: {
130
+ [WebRTCState.DISCONNECTED]: {
156
131
  on: {
157
- [WebRtcFsmEvent.CONNECT]: WebRtcState.CONNECTING,
158
- [WebRtcFsmEvent.RECONNECTING]: WebRtcState.RECONNECTING,
159
- [WebRtcFsmEvent.RESET]: WebRtcState.IDLE,
132
+ [WebRTCFsmEvent.CONNECT]: WebRTCState.CONNECTING,
133
+ [WebRTCFsmEvent.RECONNECTING]: WebRTCState.RECONNECTING,
134
+ [WebRTCFsmEvent.RESET]: WebRTCState.IDLE,
160
135
  },
161
136
  },
162
- [WebRtcState.ERROR]: {
163
- on: { [WebRtcFsmEvent.RESET]: WebRtcState.IDLE },
137
+ [WebRTCState.ERROR]: {
138
+ on: { [WebRTCFsmEvent.RESET]: WebRTCState.IDLE },
164
139
  },
165
140
  },
166
141
  });
@@ -220,11 +195,11 @@ export class WebRtcManager {
220
195
  handler(getCurrentState());
221
196
  // Subscribe to relevant events that affect the overall state
222
197
  const unsubscribers = [
223
- this.#pubsub.subscribe(WebRtcManager.EVENT_STATE_CHANGE, () => handler(getCurrentState())),
224
- this.#pubsub.subscribe(WebRtcManager.EVENT_LOCAL_STREAM, () => handler(getCurrentState())),
225
- this.#pubsub.subscribe(WebRtcManager.EVENT_REMOTE_STREAM, () => handler(getCurrentState())),
226
- this.#pubsub.subscribe(WebRtcManager.EVENT_DATA_CHANNEL_OPEN, () => handler(getCurrentState())),
227
- this.#pubsub.subscribe(WebRtcManager.EVENT_DATA_CHANNEL_CLOSE, () => handler(getCurrentState())),
198
+ this.#pubsub.subscribe(WebRTCManager.EVENT_STATE_CHANGE, () => handler(getCurrentState())),
199
+ this.#pubsub.subscribe(WebRTCManager.EVENT_LOCAL_STREAM, () => handler(getCurrentState())),
200
+ this.#pubsub.subscribe(WebRTCManager.EVENT_REMOTE_STREAM, () => handler(getCurrentState())),
201
+ this.#pubsub.subscribe(WebRTCManager.EVENT_DATA_CHANNEL_OPEN, () => handler(getCurrentState())),
202
+ this.#pubsub.subscribe(WebRTCManager.EVENT_DATA_CHANNEL_CLOSE, () => handler(getCurrentState())),
228
203
  ];
229
204
  // Return combined unsubscribe function
230
205
  return () => {
@@ -284,7 +259,7 @@ export class WebRtcManager {
284
259
  this.#localStream.getAudioTracks().forEach((track) => track.stop());
285
260
  // Update local stream reference
286
261
  this.#localStream = newStream;
287
- this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, newStream);
262
+ this.#pubsub.publish(WebRTCManager.EVENT_LOCAL_STREAM, newStream);
288
263
  return true;
289
264
  }
290
265
  catch (e) {
@@ -298,23 +273,23 @@ export class WebRtcManager {
298
273
  * Must be called before creating offers or answers. Can only be called from IDLE state.
299
274
  */
300
275
  async initialize() {
301
- if (this.state !== WebRtcState.IDLE) {
302
- this.#logDebug("initialize() called but state is not IDLE:", this.state);
276
+ if (this.state !== WebRTCState.IDLE) {
277
+ this.#logger.debug("initialize() called but state is not IDLE:", this.state);
303
278
  return;
304
279
  }
305
- this.#logDebug("Initializing...");
306
- this.#dispatch(WebRtcFsmEvent.INIT);
280
+ this.#logger.debug("Initializing...");
281
+ this.#dispatch(WebRTCFsmEvent.INIT);
307
282
  try {
308
283
  this.#pc = this.#factory.createPeerConnection(this.#config.peerConfig);
309
- this.#logDebug("Peer connection created");
284
+ this.#logger.debug("Peer connection created");
310
285
  this.#setupPcListeners();
311
286
  // Setup device change detection now that we have a connection
312
287
  this.#setupDeviceChangeListener();
313
288
  if (this.#config.enableMicrophone) {
314
- this.#logDebug("Enabling microphone (config enabled)");
289
+ this.#logger.debug("Enabling microphone (config enabled)");
315
290
  const success = await this.enableMicrophone(true);
316
291
  if (!success) {
317
- this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
292
+ this.#pubsub.publish(WebRTCManager.EVENT_MICROPHONE_FAILED, {
318
293
  reason: "Failed to enable microphone during initialization",
319
294
  });
320
295
  }
@@ -323,13 +298,13 @@ export class WebRtcManager {
323
298
  // Always setup to receive audio, even if we don't enable microphone
324
299
  // This ensures the SDP includes audio media line
325
300
  this.#pc.addTransceiver("audio", { direction: "recvonly" });
326
- this.#logDebug("Added recvonly audio transceiver");
301
+ this.#logger.debug("Added recvonly audio transceiver");
327
302
  }
328
303
  if (this.#config.dataChannelLabel) {
329
- this.#logDebug("Creating default data channel:", this.#config.dataChannelLabel);
304
+ this.#logger.debug("Creating default data channel:", this.#config.dataChannelLabel);
330
305
  this.createDataChannel(this.#config.dataChannelLabel);
331
306
  }
332
- this.#logDebug("Initialization complete");
307
+ this.#logger.debug("Initialization complete");
333
308
  }
334
309
  catch (e) {
335
310
  this.#logError(e);
@@ -341,30 +316,30 @@ export class WebRtcManager {
341
316
  * If disconnected, reinitializes the peer connection.
342
317
  */
343
318
  async connect() {
344
- this.#logDebug("connect() called, current state:", this.state);
319
+ this.#logger.debug("connect() called, current state:", this.state);
345
320
  // Initialize if needed
346
- if (this.state === WebRtcState.IDLE) {
347
- this.#logDebug("State is IDLE, initializing first");
321
+ if (this.state === WebRTCState.IDLE) {
322
+ this.#logger.debug("State is IDLE, initializing first");
348
323
  await this.initialize();
349
324
  }
350
325
  // Reinitialize if disconnected (PeerConnection was closed)
351
- if (this.state === WebRtcState.DISCONNECTED) {
352
- this.#logDebug("State is DISCONNECTED, reinitializing");
326
+ if (this.state === WebRTCState.DISCONNECTED) {
327
+ this.#logger.debug("State is DISCONNECTED, reinitializing");
353
328
  // Clean up old connection
354
329
  this.#cleanup();
355
330
  // Reset to IDLE and reinitialize
356
- this.#fsm.transition(WebRtcFsmEvent.RESET);
331
+ this.#fsm.transition(WebRTCFsmEvent.RESET);
357
332
  await this.initialize();
358
333
  // Stay in INITIALIZING state - caller needs to create offer/answer
359
334
  return;
360
335
  }
361
- if (this.state === WebRtcState.CONNECTED ||
362
- this.state === WebRtcState.CONNECTING) {
363
- this.#logDebug("Already connected or connecting, skipping");
336
+ if (this.state === WebRTCState.CONNECTED ||
337
+ this.state === WebRTCState.CONNECTING) {
338
+ this.#logger.debug("Already connected or connecting, skipping");
364
339
  return;
365
340
  }
366
- this.#logDebug("Transitioning to CONNECTING");
367
- this.#dispatch(WebRtcFsmEvent.CONNECT);
341
+ this.#logger.debug("Transitioning to CONNECTING");
342
+ this.#dispatch(WebRTCFsmEvent.CONNECT);
368
343
  }
369
344
  /**
370
345
  * Enables or disables the microphone and adds/removes audio tracks to the peer connection.
@@ -372,21 +347,21 @@ export class WebRtcManager {
372
347
  * @returns True if successful, false if failed to get user media.
373
348
  */
374
349
  async enableMicrophone(enable) {
375
- this.#logDebug("enableMicrophone() called:", enable);
350
+ this.#logger.debug("enableMicrophone() called:", enable);
376
351
  if (enable) {
377
352
  if (this.#localStream) {
378
- this.#logDebug("Microphone already enabled");
353
+ this.#logger.debug("Microphone already enabled");
379
354
  return true;
380
355
  }
381
356
  try {
382
- this.#logDebug("Requesting user media...");
357
+ this.#logger.debug("Requesting user media...");
383
358
  const stream = await this.#factory.getUserMedia({
384
359
  audio: true,
385
360
  video: false,
386
361
  });
387
- this.#logDebug("User media obtained, tracks:", stream.getAudioTracks().length);
362
+ this.#logger.debug("User media obtained, tracks:", stream.getAudioTracks().length);
388
363
  this.#localStream = stream;
389
- this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, stream);
364
+ this.#pubsub.publish(WebRTCManager.EVENT_LOCAL_STREAM, stream);
390
365
  if (this.#pc) {
391
366
  // Check if we have an existing audio transceiver
392
367
  const transceivers = this.#pc.getTransceivers();
@@ -397,22 +372,22 @@ export class WebRtcManager {
397
372
  await audioTransceiver.sender.replaceTrack(track);
398
373
  // Update direction to sendrecv
399
374
  audioTransceiver.direction = "sendrecv";
400
- this.#logDebug("Replaced track in existing transceiver");
375
+ this.#logger.debug("Replaced track in existing transceiver");
401
376
  }
402
377
  else {
403
378
  // Add track normally
404
379
  stream.getTracks().forEach((track) => {
405
380
  this.#pc.addTrack(track, stream);
406
381
  });
407
- this.#logDebug("Added tracks to peer connection");
382
+ this.#logger.debug("Added tracks to peer connection");
408
383
  }
409
384
  }
410
- this.#logDebug("Microphone enabled successfully");
385
+ this.#logger.debug("Microphone enabled successfully");
411
386
  return true;
412
387
  }
413
388
  catch (e) {
414
389
  this.#logError("Failed to get user media:", e);
415
- this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
390
+ this.#pubsub.publish(WebRTCManager.EVENT_MICROPHONE_FAILED, {
416
391
  error: e,
417
392
  });
418
393
  return false;
@@ -420,10 +395,10 @@ export class WebRtcManager {
420
395
  }
421
396
  else {
422
397
  if (!this.#localStream) {
423
- this.#logDebug("Microphone already disabled");
398
+ this.#logger.debug("Microphone already disabled");
424
399
  return true;
425
400
  }
426
- this.#logDebug("Disabling microphone...");
401
+ this.#logger.debug("Disabling microphone...");
427
402
  this.#localStream.getTracks().forEach((track) => {
428
403
  track.stop();
429
404
  // Remove from PC if needed, or just stop sending
@@ -436,8 +411,8 @@ export class WebRtcManager {
436
411
  }
437
412
  });
438
413
  this.#localStream = null;
439
- this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, null);
440
- this.#logDebug("Microphone disabled");
414
+ this.#pubsub.publish(WebRTCManager.EVENT_LOCAL_STREAM, null);
415
+ this.#logger.debug("Microphone disabled");
441
416
  return true;
442
417
  }
443
418
  }
@@ -446,32 +421,32 @@ export class WebRtcManager {
446
421
  * Transitions to DISCONNECTED state.
447
422
  */
448
423
  disconnect() {
449
- this.#logDebug("disconnect() called");
424
+ this.#logger.debug("disconnect() called");
450
425
  this.#cleanup();
451
- this.#dispatch(WebRtcFsmEvent.DISCONNECT);
426
+ this.#dispatch(WebRTCFsmEvent.DISCONNECT);
452
427
  }
453
428
  /**
454
429
  * Resets the manager to IDLE state from any state.
455
430
  * Cleans up all resources and allows reinitialization.
456
431
  */
457
432
  reset() {
458
- this.#logDebug("reset() called, current state:", this.state);
433
+ this.#logger.debug("reset() called, current state:", this.state);
459
434
  this.#cleanup();
460
435
  // Reset from any non-IDLE state
461
- if (this.state !== WebRtcState.IDLE) {
436
+ if (this.state !== WebRTCState.IDLE) {
462
437
  // Force transition to DISCONNECTED first if needed, then to IDLE
463
- if (this.state === WebRtcState.ERROR ||
464
- this.state === WebRtcState.DISCONNECTED ||
465
- this.state === WebRtcState.RECONNECTING) {
466
- this.#dispatch(WebRtcFsmEvent.RESET);
438
+ if (this.state === WebRTCState.ERROR ||
439
+ this.state === WebRTCState.DISCONNECTED ||
440
+ this.state === WebRTCState.RECONNECTING) {
441
+ this.#dispatch(WebRTCFsmEvent.RESET);
467
442
  }
468
443
  else {
469
444
  // For other states, go through DISCONNECTED first
470
- this.#dispatch(WebRtcFsmEvent.DISCONNECT);
471
- this.#dispatch(WebRtcFsmEvent.RESET);
445
+ this.#dispatch(WebRTCFsmEvent.DISCONNECT);
446
+ this.#dispatch(WebRTCFsmEvent.RESET);
472
447
  }
473
448
  }
474
- this.#logDebug("Reset complete, state:", this.state);
449
+ this.#logger.debug("Reset complete, state:", this.state);
475
450
  }
476
451
  /**
477
452
  * Creates a new data channel with the specified label.
@@ -481,20 +456,20 @@ export class WebRtcManager {
481
456
  * @returns The created data channel, or null if peer connection not initialized.
482
457
  */
483
458
  createDataChannel(label, options) {
484
- this.#logDebug("createDataChannel() called:", label);
459
+ this.#logger.debug("createDataChannel() called:", label);
485
460
  if (!this.#pc) {
486
- this.#logDebug("Cannot create data channel: peer connection not initialized");
461
+ this.#logger.debug("Cannot create data channel: peer connection not initialized");
487
462
  return null;
488
463
  }
489
464
  if (this.#dataChannels.has(label)) {
490
- this.#logDebug("Returning existing data channel:", label);
465
+ this.#logger.debug("Returning existing data channel:", label);
491
466
  return this.#dataChannels.get(label);
492
467
  }
493
468
  try {
494
469
  const dc = this.#pc.createDataChannel(label, options);
495
470
  this.#setupDataChannelListeners(dc);
496
471
  this.#dataChannels.set(label, dc);
497
- this.#logDebug("Data channel created:", label);
472
+ this.#logger.debug("Data channel created:", label);
498
473
  return dc;
499
474
  }
500
475
  catch (e) {
@@ -521,11 +496,11 @@ export class WebRtcManager {
521
496
  sendData(label, data) {
522
497
  const channel = this.#dataChannels.get(label);
523
498
  if (!channel) {
524
- this.#logDebug(`Data channel '${label}' not found`);
499
+ this.#logger.debug(`Data channel '${label}' not found`);
525
500
  return false;
526
501
  }
527
502
  if (channel.readyState !== "open") {
528
- this.#logDebug(`Data channel '${label}' is not open (state: ${channel.readyState})`);
503
+ this.#logger.debug(`Data channel '${label}' is not open (state: ${channel.readyState})`);
529
504
  return false;
530
505
  }
531
506
  try {
@@ -546,14 +521,14 @@ export class WebRtcManager {
546
521
  * @returns The offer SDP, or null if peer connection not initialized.
547
522
  */
548
523
  async createOffer(options) {
549
- this.#logDebug("createOffer() called");
524
+ this.#logger.debug("createOffer() called");
550
525
  if (!this.#pc) {
551
- this.#logDebug("Cannot create offer: peer connection not initialized");
526
+ this.#logger.debug("Cannot create offer: peer connection not initialized");
552
527
  return null;
553
528
  }
554
529
  try {
555
530
  const offer = await this.#pc.createOffer(options);
556
- this.#logDebug("Offer created:", offer.type);
531
+ this.#logger.debug("Offer created:", offer.type);
557
532
  return offer;
558
533
  }
559
534
  catch (e) {
@@ -568,14 +543,14 @@ export class WebRtcManager {
568
543
  * @returns The answer SDP, or null if peer connection not initialized.
569
544
  */
570
545
  async createAnswer(options) {
571
- this.#logDebug("createAnswer() called");
546
+ this.#logger.debug("createAnswer() called");
572
547
  if (!this.#pc) {
573
- this.#logDebug("Cannot create answer: peer connection not initialized");
548
+ this.#logger.debug("Cannot create answer: peer connection not initialized");
574
549
  return null;
575
550
  }
576
551
  try {
577
552
  const answer = await this.#pc.createAnswer(options);
578
- this.#logDebug("Answer created:", answer.type);
553
+ this.#logger.debug("Answer created:", answer.type);
579
554
  return answer;
580
555
  }
581
556
  catch (e) {
@@ -590,14 +565,14 @@ export class WebRtcManager {
590
565
  * @returns True if successful, false otherwise.
591
566
  */
592
567
  async setLocalDescription(description) {
593
- this.#logDebug("setLocalDescription() called:", description.type);
568
+ this.#logger.debug("setLocalDescription() called:", description.type);
594
569
  if (!this.#pc) {
595
- this.#logDebug("Cannot set local description: peer connection not initialized");
570
+ this.#logger.debug("Cannot set local description: peer connection not initialized");
596
571
  return false;
597
572
  }
598
573
  try {
599
574
  await this.#pc.setLocalDescription(description);
600
- this.#logDebug("Local description set successfully");
575
+ this.#logger.debug("Local description set successfully");
601
576
  return true;
602
577
  }
603
578
  catch (e) {
@@ -612,14 +587,14 @@ export class WebRtcManager {
612
587
  * @returns True if successful, false otherwise.
613
588
  */
614
589
  async setRemoteDescription(description) {
615
- this.#logDebug("setRemoteDescription() called:", description.type);
590
+ this.#logger.debug("setRemoteDescription() called:", description.type);
616
591
  if (!this.#pc) {
617
- this.#logDebug("Cannot set remote description: peer connection not initialized");
592
+ this.#logger.debug("Cannot set remote description: peer connection not initialized");
618
593
  return false;
619
594
  }
620
595
  try {
621
596
  await this.#pc.setRemoteDescription(description);
622
- this.#logDebug("Remote description set successfully");
597
+ this.#logger.debug("Remote description set successfully");
623
598
  return true;
624
599
  }
625
600
  catch (e) {
@@ -634,15 +609,15 @@ export class WebRtcManager {
634
609
  * @returns True if successful, false otherwise.
635
610
  */
636
611
  async addIceCandidate(candidate) {
637
- this.#logDebug("addIceCandidate() called:", candidate ? "candidate" : "null (end-of-candidates)");
612
+ this.#logger.debug("addIceCandidate() called:", candidate ? "candidate" : "null (end-of-candidates)");
638
613
  if (!this.#pc) {
639
- this.#logDebug("Cannot add ICE candidate: peer connection not initialized");
614
+ this.#logger.debug("Cannot add ICE candidate: peer connection not initialized");
640
615
  return false;
641
616
  }
642
617
  try {
643
618
  if (candidate) {
644
619
  await this.#pc.addIceCandidate(candidate);
645
- this.#logDebug("ICE candidate added");
620
+ this.#logger.debug("ICE candidate added");
646
621
  }
647
622
  return true;
648
623
  }
@@ -658,15 +633,15 @@ export class WebRtcManager {
658
633
  * @returns True if successful, false otherwise.
659
634
  */
660
635
  async iceRestart() {
661
- this.#logDebug("iceRestart() called");
636
+ this.#logger.debug("iceRestart() called");
662
637
  if (!this.#pc) {
663
- this.#logDebug("Cannot perform ICE restart: peer connection not initialized");
638
+ this.#logger.debug("Cannot perform ICE restart: peer connection not initialized");
664
639
  return false;
665
640
  }
666
641
  try {
667
642
  const offer = await this.#pc.createOffer({ iceRestart: true });
668
643
  await this.#pc.setLocalDescription(offer);
669
- this.#logDebug("ICE restart initiated");
644
+ this.#logger.debug("ICE restart initiated");
670
645
  return true;
671
646
  }
672
647
  catch (e) {
@@ -688,10 +663,10 @@ export class WebRtcManager {
688
663
  }
689
664
  const pc = this.#pc;
690
665
  if (pc.iceGatheringState === "complete") {
691
- this.#logDebug("ICE gathering already complete");
666
+ this.#logger.debug("ICE gathering already complete");
692
667
  return Promise.resolve();
693
668
  }
694
- this.#logDebug("Waiting for ICE gathering to complete...");
669
+ this.#logger.debug("Waiting for ICE gathering to complete...");
695
670
  return new Promise((resolve, reject) => {
696
671
  const timer = setTimeout(() => {
697
672
  cleanup();
@@ -704,7 +679,7 @@ export class WebRtcManager {
704
679
  };
705
680
  const checkState = () => {
706
681
  if (pc.iceGatheringState === "complete") {
707
- this.#logDebug("ICE gathering complete (via state change)");
682
+ this.#logger.debug("ICE gathering complete (via state change)");
708
683
  cleanup();
709
684
  resolve();
710
685
  }
@@ -712,7 +687,7 @@ export class WebRtcManager {
712
687
  const handleCandidate = (event) => {
713
688
  onCandidate?.(event.candidate);
714
689
  if (event.candidate === null) {
715
- this.#logDebug("ICE gathering complete (null candidate)");
690
+ this.#logger.debug("ICE gathering complete (null candidate)");
716
691
  cleanup();
717
692
  resolve();
718
693
  }
@@ -757,54 +732,48 @@ export class WebRtcManager {
757
732
  this.#fsm.transition(event);
758
733
  const newState = this.#fsm.state;
759
734
  if (oldState !== newState) {
760
- this.#logDebug("State transition:", oldState, "->", newState, "(event:", event + ")");
761
- this.#pubsub.publish(WebRtcManager.EVENT_STATE_CHANGE, newState);
762
- }
763
- }
764
- // deno-lint-ignore no-explicit-any
765
- #logDebug(...args) {
766
- if (this.#config.debug) {
767
- this.#logger.debug("[WebRtcManager]", ...args);
735
+ this.#logger.debug("State transition:", oldState, "->", newState, "(event:", event + ")");
736
+ this.#pubsub.publish(WebRTCManager.EVENT_STATE_CHANGE, newState);
768
737
  }
769
738
  }
770
739
  // deno-lint-ignore no-explicit-any
771
740
  #log(...args) {
772
- this.#logger.log("[WebRtcManager]", ...args);
741
+ this.#logger.log(...args);
773
742
  }
774
743
  // deno-lint-ignore no-explicit-any
775
744
  #logWarn(...args) {
776
- this.#logger.warn("[WebRtcManager]", ...args);
745
+ this.#logger.warn(...args);
777
746
  }
778
747
  // deno-lint-ignore no-explicit-any
779
748
  #logError(...args) {
780
- this.#logger.error("[WebRtcManager]", ...args);
749
+ this.#logger.error(...args);
781
750
  }
782
751
  // deno-lint-ignore no-explicit-any
783
752
  #handleError(error) {
784
- this.#dispatch(WebRtcFsmEvent.ERROR);
785
- this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
753
+ this.#dispatch(WebRTCFsmEvent.ERROR);
754
+ this.#pubsub.publish(WebRTCManager.EVENT_ERROR, error);
786
755
  }
787
756
  #setupPcListeners() {
788
757
  if (!this.#pc)
789
758
  return;
790
- this.#logDebug("Setting up peer connection listeners");
759
+ this.#logger.debug("Setting up peer connection listeners");
791
760
  this.#pc.onconnectionstatechange = () => {
792
761
  const state = this.#pc.connectionState;
793
- this.#logDebug("Connection state changed:", state);
762
+ this.#logger.debug("Connection state changed:", state);
794
763
  if (state === "connected") {
795
764
  // Only dispatch if in CONNECTING state (FSM can handle CONNECTED event)
796
765
  // This guards against late connection success after user has disconnected
797
- if (this.state === WebRtcState.CONNECTING) {
766
+ if (this.state === WebRTCState.CONNECTING) {
798
767
  // Connection successful - reset reconnect attempts and clear any pending timeout
799
768
  this.#reconnectAttempts = 0;
800
769
  if (this.#fullReconnectTimeoutTimer !== null) {
801
770
  clearTimeout(this.#fullReconnectTimeoutTimer);
802
771
  this.#fullReconnectTimeoutTimer = null;
803
772
  }
804
- this.#dispatch(WebRtcFsmEvent.CONNECTED);
773
+ this.#dispatch(WebRTCFsmEvent.CONNECTED);
805
774
  }
806
775
  else {
807
- this.#logDebug(`Ignoring late connection success (current state: ${this.state})`);
776
+ this.#logger.debug(`Ignoring late connection success (current state: ${this.state})`);
808
777
  }
809
778
  }
810
779
  else if (state === "failed") {
@@ -813,33 +782,33 @@ export class WebRtcManager {
813
782
  }
814
783
  else if (state === "disconnected" || state === "closed") {
815
784
  // Only dispatch if not already in a terminal state
816
- if (this.state !== WebRtcState.DISCONNECTED &&
817
- this.state !== WebRtcState.ERROR &&
818
- this.state !== WebRtcState.IDLE) {
819
- this.#dispatch(WebRtcFsmEvent.DISCONNECT);
785
+ if (this.state !== WebRTCState.DISCONNECTED &&
786
+ this.state !== WebRTCState.ERROR &&
787
+ this.state !== WebRTCState.IDLE) {
788
+ this.#dispatch(WebRTCFsmEvent.DISCONNECT);
820
789
  }
821
790
  }
822
791
  };
823
792
  this.#pc.ontrack = (event) => {
824
- this.#logDebug("Remote track received:", event.track.kind);
793
+ this.#logger.debug("Remote track received:", event.track.kind);
825
794
  if (event.streams && event.streams[0]) {
826
795
  this.#remoteStream = event.streams[0];
827
- this.#pubsub.publish(WebRtcManager.EVENT_REMOTE_STREAM, this.#remoteStream);
796
+ this.#pubsub.publish(WebRTCManager.EVENT_REMOTE_STREAM, this.#remoteStream);
828
797
  }
829
798
  };
830
799
  this.#pc.ondatachannel = (event) => {
831
800
  const dc = event.channel;
832
- this.#logDebug("Remote data channel received:", dc.label);
801
+ this.#logger.debug("Remote data channel received:", dc.label);
833
802
  this.#setupDataChannelListeners(dc);
834
803
  this.#dataChannels.set(dc.label, dc);
835
804
  };
836
805
  this.#pc.onicecandidate = (event) => {
837
- this.#logDebug("ICE candidate generated:", event.candidate ? "candidate" : "null (gathering complete)");
838
- this.#pubsub.publish(WebRtcManager.EVENT_ICE_CANDIDATE, event.candidate);
806
+ this.#logger.debug("ICE candidate generated:", event.candidate ? "candidate" : "null (gathering complete)");
807
+ this.#pubsub.publish(WebRTCManager.EVENT_ICE_CANDIDATE, event.candidate);
839
808
  };
840
809
  }
841
810
  #cleanup() {
842
- this.#logDebug("Cleanup started");
811
+ this.#logger.debug("Cleanup started");
843
812
  // Clear any pending reconnect timers
844
813
  if (this.#reconnectTimer !== null) {
845
814
  clearTimeout(this.#reconnectTimer);
@@ -864,41 +833,41 @@ export class WebRtcManager {
864
833
  });
865
834
  this.#dataChannels.clear();
866
835
  if (dcCount > 0) {
867
- this.#logDebug("Closed", dcCount, "data channel(s)");
836
+ this.#logger.debug("Closed", dcCount, "data channel(s)");
868
837
  }
869
838
  // Stop local stream tracks
870
839
  if (this.#localStream) {
871
840
  this.#localStream.getTracks().forEach((track) => track.stop());
872
841
  this.#localStream = null;
873
- this.#logDebug("Local stream stopped");
842
+ this.#logger.debug("Local stream stopped");
874
843
  }
875
844
  // Close peer connection
876
845
  if (this.#pc) {
877
846
  this.#pc.close();
878
847
  this.#pc = null;
879
- this.#logDebug("Peer connection closed");
848
+ this.#logger.debug("Peer connection closed");
880
849
  }
881
850
  this.#remoteStream = null;
882
- this.#logDebug("Cleanup complete");
851
+ this.#logger.debug("Cleanup complete");
883
852
  }
884
853
  #handleConnectionFailure() {
885
- this.#logDebug("Handling connection failure");
854
+ this.#logger.debug("Handling connection failure");
886
855
  // Only dispatch DISCONNECT if not already in a terminal state
887
- if (this.state !== WebRtcState.DISCONNECTED &&
888
- this.state !== WebRtcState.ERROR &&
889
- this.state !== WebRtcState.IDLE) {
890
- this.#dispatch(WebRtcFsmEvent.DISCONNECT);
856
+ if (this.state !== WebRTCState.DISCONNECTED &&
857
+ this.state !== WebRTCState.ERROR &&
858
+ this.state !== WebRTCState.IDLE) {
859
+ this.#dispatch(WebRTCFsmEvent.DISCONNECT);
891
860
  }
892
861
  // Check if auto-reconnect is enabled
893
862
  if (!this.#config.autoReconnect) {
894
- this.#logDebug("Auto-reconnect disabled, not attempting reconnection");
863
+ this.#logger.debug("Auto-reconnect disabled, not attempting reconnection");
895
864
  return;
896
865
  }
897
866
  const maxAttempts = this.#config.maxReconnectAttempts ?? 5;
898
867
  // Check if we've exceeded max attempts
899
868
  if (this.#reconnectAttempts >= maxAttempts) {
900
- this.#logDebug("Max reconnection attempts reached:", maxAttempts);
901
- this.#pubsub.publish(WebRtcManager.EVENT_RECONNECT_FAILED, {
869
+ this.#logger.debug("Max reconnection attempts reached:", maxAttempts);
870
+ this.#pubsub.publish(WebRTCManager.EVENT_RECONNECT_FAILED, {
902
871
  attempts: this.#reconnectAttempts,
903
872
  });
904
873
  return;
@@ -914,12 +883,12 @@ export class WebRtcManager {
914
883
  strategy,
915
884
  });
916
885
  if (!shouldProceed) {
917
- this.#logDebug("Reconnection suppressed by shouldReconnect callback");
886
+ this.#logger.debug("Reconnection suppressed by shouldReconnect callback");
918
887
  return;
919
888
  }
920
889
  }
921
890
  // Transition to RECONNECTING state
922
- this.#dispatch(WebRtcFsmEvent.RECONNECTING);
891
+ this.#dispatch(WebRTCFsmEvent.RECONNECTING);
923
892
  // Attempt reconnection with exponential backoff
924
893
  this.#attemptReconnect();
925
894
  }
@@ -929,12 +898,12 @@ export class WebRtcManager {
929
898
  const delay = baseDelay * Math.pow(2, this.#reconnectAttempts - 1);
930
899
  // Try ICE restart first (attempts 1-2), then full reconnect
931
900
  const strategy = this.#reconnectAttempts <= 2 ? "ice-restart" : "full";
932
- this.#logDebug("Attempting reconnection:", {
901
+ this.#logger.debug("Attempting reconnection:", {
933
902
  attempt: this.#reconnectAttempts,
934
903
  strategy,
935
904
  delay: delay + "ms",
936
905
  });
937
- this.#pubsub.publish(WebRtcManager.EVENT_RECONNECTING, {
906
+ this.#pubsub.publish(WebRTCManager.EVENT_RECONNECTING, {
938
907
  attempt: this.#reconnectAttempts,
939
908
  strategy,
940
909
  });
@@ -958,7 +927,7 @@ export class WebRtcManager {
958
927
  try {
959
928
  // Clean up old connection and reset to IDLE so connect() creates a new PC
960
929
  this.#cleanup();
961
- this.#dispatch(WebRtcFsmEvent.RESET);
930
+ this.#dispatch(WebRTCFsmEvent.RESET);
962
931
  await this.connect();
963
932
  // Start timeout for full reconnection - if connection doesn't succeed
964
933
  // within the timeout, treat it as a failure
@@ -966,8 +935,8 @@ export class WebRtcManager {
966
935
  this.#fullReconnectTimeoutTimer = setTimeout(() => {
967
936
  this.#fullReconnectTimeoutTimer = null;
968
937
  // Only trigger failure if still not connected
969
- if (this.state !== WebRtcState.CONNECTED) {
970
- this.#logDebug("Full reconnection timeout reached, connection not established");
938
+ if (this.state !== WebRTCState.CONNECTED) {
939
+ this.#logger.debug("Full reconnection timeout reached, connection not established");
971
940
  this.#handleConnectionFailure();
972
941
  }
973
942
  }, timeout);
@@ -991,7 +960,7 @@ export class WebRtcManager {
991
960
  this.#deviceChangeHandler = async () => {
992
961
  try {
993
962
  const devices = await this.getAudioInputDevices();
994
- this.#pubsub.publish(WebRtcManager.EVENT_DEVICE_CHANGED, devices);
963
+ this.#pubsub.publish(WebRTCManager.EVENT_DEVICE_CHANGED, devices);
995
964
  }
996
965
  catch (e) {
997
966
  this.#logError("Error handling device change:", e);
@@ -1001,16 +970,16 @@ export class WebRtcManager {
1001
970
  }
1002
971
  #setupDataChannelListeners(dc) {
1003
972
  dc.onopen = () => {
1004
- this.#pubsub.publish(WebRtcManager.EVENT_DATA_CHANNEL_OPEN, dc);
973
+ this.#pubsub.publish(WebRTCManager.EVENT_DATA_CHANNEL_OPEN, dc);
1005
974
  };
1006
975
  dc.onmessage = (event) => {
1007
- this.#pubsub.publish(WebRtcManager.EVENT_DATA_CHANNEL_MESSAGE, {
976
+ this.#pubsub.publish(WebRTCManager.EVENT_DATA_CHANNEL_MESSAGE, {
1008
977
  channel: dc,
1009
978
  data: event.data,
1010
979
  });
1011
980
  };
1012
981
  dc.onclose = () => {
1013
- this.#pubsub.publish(WebRtcManager.EVENT_DATA_CHANNEL_CLOSE, dc);
982
+ this.#pubsub.publish(WebRTCManager.EVENT_DATA_CHANNEL_CLOSE, dc);
1014
983
  this.#dataChannels.delete(dc.label);
1015
984
  };
1016
985
  // deno-lint-ignore no-explicit-any
@@ -1019,7 +988,7 @@ export class WebRtcManager {
1019
988
  const isUserAbort = error?.error?.message?.includes("User-Initiated Abort");
1020
989
  if (!isUserAbort) {
1021
990
  this.#logError("Data channel error:", error);
1022
- this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
991
+ this.#pubsub.publish(WebRTCManager.EVENT_ERROR, error);
1023
992
  }
1024
993
  };
1025
994
  }