@marianmeres/webrtc 1.2.4 → 1.2.6
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 +48 -0
- package/dist/webrtc-manager.js +101 -88
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -105,6 +105,54 @@ const dc = manager.getDataChannel(label)
|
|
|
105
105
|
manager.sendData(label, data) // Returns boolean
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
### Working with External Audio Streams
|
|
109
|
+
|
|
110
|
+
You might want to keep `enableMicrophone: false` even when your application uses audio. Common scenarios include:
|
|
111
|
+
|
|
112
|
+
- **Pre-acquired stream**: You may have obtained the audio stream earlier in the application flow (e.g., during a permissions check, in a lobby/waiting room, or for local audio preview before joining)
|
|
113
|
+
- **Custom audio processing**: You want to apply audio effects, noise suppression, or other processing via Web Audio API before transmitting
|
|
114
|
+
- **Multiple sources**: You need to mix audio from multiple sources (microphone + system audio, multiple microphones, background music, etc.)
|
|
115
|
+
- **Fine-grained privacy control**: You want explicit control over exactly when the microphone activates
|
|
116
|
+
- **Testing**: You want to inject synthetic audio (e.g., oscillator tones) for automated testing
|
|
117
|
+
|
|
118
|
+
To use your own audio stream, access the `peerConnection` directly and add tracks after initialization:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const manager = new WebRtcManager(factory, {
|
|
122
|
+
peerConfig: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] },
|
|
123
|
+
enableMicrophone: false, // We'll handle the audio stream ourselves
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Your pre-acquired or processed audio stream
|
|
127
|
+
const myAudioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
128
|
+
|
|
129
|
+
// Or a processed stream via Web Audio API
|
|
130
|
+
const audioCtx = new AudioContext();
|
|
131
|
+
const source = audioCtx.createMediaStreamSource(myAudioStream);
|
|
132
|
+
const gainNode = audioCtx.createGain();
|
|
133
|
+
gainNode.gain.value = 0.8;
|
|
134
|
+
source.connect(gainNode);
|
|
135
|
+
const destination = audioCtx.createMediaStreamDestination();
|
|
136
|
+
gainNode.connect(destination);
|
|
137
|
+
const processedStream = destination.stream;
|
|
138
|
+
|
|
139
|
+
// Initialize the manager
|
|
140
|
+
await manager.initialize();
|
|
141
|
+
|
|
142
|
+
// Add your audio track to the peer connection
|
|
143
|
+
const pc = manager.peerConnection;
|
|
144
|
+
if (pc) {
|
|
145
|
+
processedStream.getAudioTracks().forEach((track) => {
|
|
146
|
+
pc.addTrack(track, processedStream);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Continue with normal connection flow
|
|
151
|
+
await manager.connect();
|
|
152
|
+
const offer = await manager.createOffer();
|
|
153
|
+
// ...
|
|
154
|
+
```
|
|
155
|
+
|
|
108
156
|
### Event Subscription
|
|
109
157
|
|
|
110
158
|
```typescript
|
package/dist/webrtc-manager.js
CHANGED
|
@@ -241,7 +241,7 @@ export class WebRtcManager {
|
|
|
241
241
|
return devices.filter((d) => d.kind === "audioinput");
|
|
242
242
|
}
|
|
243
243
|
catch (e) {
|
|
244
|
-
this.#
|
|
244
|
+
this.#logError("Failed to enumerate devices:", e);
|
|
245
245
|
return [];
|
|
246
246
|
}
|
|
247
247
|
}
|
|
@@ -252,7 +252,7 @@ export class WebRtcManager {
|
|
|
252
252
|
*/
|
|
253
253
|
async switchMicrophone(deviceId) {
|
|
254
254
|
if (!this.#pc || !this.#localStream) {
|
|
255
|
-
this.#
|
|
255
|
+
this.#logError("Cannot switch microphone: not initialized or no active stream");
|
|
256
256
|
return false;
|
|
257
257
|
}
|
|
258
258
|
try {
|
|
@@ -288,8 +288,8 @@ export class WebRtcManager {
|
|
|
288
288
|
return true;
|
|
289
289
|
}
|
|
290
290
|
catch (e) {
|
|
291
|
-
this.#
|
|
292
|
-
this.#
|
|
291
|
+
this.#logError("Failed to switch microphone:", e);
|
|
292
|
+
this.#handleError(e);
|
|
293
293
|
return false;
|
|
294
294
|
}
|
|
295
295
|
}
|
|
@@ -299,19 +299,19 @@ export class WebRtcManager {
|
|
|
299
299
|
*/
|
|
300
300
|
async initialize() {
|
|
301
301
|
if (this.state !== WebRtcState.IDLE) {
|
|
302
|
-
this.#
|
|
302
|
+
this.#logDebug("initialize() called but state is not IDLE:", this.state);
|
|
303
303
|
return;
|
|
304
304
|
}
|
|
305
|
-
this.#
|
|
305
|
+
this.#logDebug("Initializing...");
|
|
306
306
|
this.#dispatch(WebRtcFsmEvent.INIT);
|
|
307
307
|
try {
|
|
308
308
|
this.#pc = this.#factory.createPeerConnection(this.#config.peerConfig);
|
|
309
|
-
this.#
|
|
309
|
+
this.#logDebug("Peer connection created");
|
|
310
310
|
this.#setupPcListeners();
|
|
311
311
|
// Setup device change detection now that we have a connection
|
|
312
312
|
this.#setupDeviceChangeListener();
|
|
313
313
|
if (this.#config.enableMicrophone) {
|
|
314
|
-
this.#
|
|
314
|
+
this.#logDebug("Enabling microphone (config enabled)");
|
|
315
315
|
const success = await this.enableMicrophone(true);
|
|
316
316
|
if (!success) {
|
|
317
317
|
this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
|
|
@@ -323,16 +323,17 @@ export class WebRtcManager {
|
|
|
323
323
|
// Always setup to receive audio, even if we don't enable microphone
|
|
324
324
|
// This ensures the SDP includes audio media line
|
|
325
325
|
this.#pc.addTransceiver("audio", { direction: "recvonly" });
|
|
326
|
-
this.#
|
|
326
|
+
this.#logDebug("Added recvonly audio transceiver");
|
|
327
327
|
}
|
|
328
328
|
if (this.#config.dataChannelLabel) {
|
|
329
|
-
this.#
|
|
329
|
+
this.#logDebug("Creating default data channel:", this.#config.dataChannelLabel);
|
|
330
330
|
this.createDataChannel(this.#config.dataChannelLabel);
|
|
331
331
|
}
|
|
332
|
-
this.#
|
|
332
|
+
this.#logDebug("Initialization complete");
|
|
333
333
|
}
|
|
334
334
|
catch (e) {
|
|
335
|
-
this.#
|
|
335
|
+
this.#logError(e);
|
|
336
|
+
this.#handleError(e);
|
|
336
337
|
}
|
|
337
338
|
}
|
|
338
339
|
/**
|
|
@@ -340,15 +341,15 @@ export class WebRtcManager {
|
|
|
340
341
|
* If disconnected, reinitializes the peer connection.
|
|
341
342
|
*/
|
|
342
343
|
async connect() {
|
|
343
|
-
this.#
|
|
344
|
+
this.#logDebug("connect() called, current state:", this.state);
|
|
344
345
|
// Initialize if needed
|
|
345
346
|
if (this.state === WebRtcState.IDLE) {
|
|
346
|
-
this.#
|
|
347
|
+
this.#logDebug("State is IDLE, initializing first");
|
|
347
348
|
await this.initialize();
|
|
348
349
|
}
|
|
349
350
|
// Reinitialize if disconnected (PeerConnection was closed)
|
|
350
351
|
if (this.state === WebRtcState.DISCONNECTED) {
|
|
351
|
-
this.#
|
|
352
|
+
this.#logDebug("State is DISCONNECTED, reinitializing");
|
|
352
353
|
// Clean up old connection
|
|
353
354
|
this.#cleanup();
|
|
354
355
|
// Reset to IDLE and reinitialize
|
|
@@ -359,10 +360,10 @@ export class WebRtcManager {
|
|
|
359
360
|
}
|
|
360
361
|
if (this.state === WebRtcState.CONNECTED ||
|
|
361
362
|
this.state === WebRtcState.CONNECTING) {
|
|
362
|
-
this.#
|
|
363
|
+
this.#logDebug("Already connected or connecting, skipping");
|
|
363
364
|
return;
|
|
364
365
|
}
|
|
365
|
-
this.#
|
|
366
|
+
this.#logDebug("Transitioning to CONNECTING");
|
|
366
367
|
this.#dispatch(WebRtcFsmEvent.CONNECT);
|
|
367
368
|
}
|
|
368
369
|
/**
|
|
@@ -371,19 +372,19 @@ export class WebRtcManager {
|
|
|
371
372
|
* @returns True if successful, false if failed to get user media.
|
|
372
373
|
*/
|
|
373
374
|
async enableMicrophone(enable) {
|
|
374
|
-
this.#
|
|
375
|
+
this.#logDebug("enableMicrophone() called:", enable);
|
|
375
376
|
if (enable) {
|
|
376
377
|
if (this.#localStream) {
|
|
377
|
-
this.#
|
|
378
|
+
this.#logDebug("Microphone already enabled");
|
|
378
379
|
return true;
|
|
379
380
|
}
|
|
380
381
|
try {
|
|
381
|
-
this.#
|
|
382
|
+
this.#logDebug("Requesting user media...");
|
|
382
383
|
const stream = await this.#factory.getUserMedia({
|
|
383
384
|
audio: true,
|
|
384
385
|
video: false,
|
|
385
386
|
});
|
|
386
|
-
this.#
|
|
387
|
+
this.#logDebug("User media obtained, tracks:", stream.getAudioTracks().length);
|
|
387
388
|
this.#localStream = stream;
|
|
388
389
|
this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, stream);
|
|
389
390
|
if (this.#pc) {
|
|
@@ -396,21 +397,21 @@ export class WebRtcManager {
|
|
|
396
397
|
await audioTransceiver.sender.replaceTrack(track);
|
|
397
398
|
// Update direction to sendrecv
|
|
398
399
|
audioTransceiver.direction = "sendrecv";
|
|
399
|
-
this.#
|
|
400
|
+
this.#logDebug("Replaced track in existing transceiver");
|
|
400
401
|
}
|
|
401
402
|
else {
|
|
402
403
|
// Add track normally
|
|
403
404
|
stream.getTracks().forEach((track) => {
|
|
404
405
|
this.#pc.addTrack(track, stream);
|
|
405
406
|
});
|
|
406
|
-
this.#
|
|
407
|
+
this.#logDebug("Added tracks to peer connection");
|
|
407
408
|
}
|
|
408
409
|
}
|
|
409
|
-
this.#
|
|
410
|
+
this.#logDebug("Microphone enabled successfully");
|
|
410
411
|
return true;
|
|
411
412
|
}
|
|
412
413
|
catch (e) {
|
|
413
|
-
this.#
|
|
414
|
+
this.#logError("Failed to get user media:", e);
|
|
414
415
|
this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
|
|
415
416
|
error: e,
|
|
416
417
|
});
|
|
@@ -419,10 +420,10 @@ export class WebRtcManager {
|
|
|
419
420
|
}
|
|
420
421
|
else {
|
|
421
422
|
if (!this.#localStream) {
|
|
422
|
-
this.#
|
|
423
|
+
this.#logDebug("Microphone already disabled");
|
|
423
424
|
return true;
|
|
424
425
|
}
|
|
425
|
-
this.#
|
|
426
|
+
this.#logDebug("Disabling microphone...");
|
|
426
427
|
this.#localStream.getTracks().forEach((track) => {
|
|
427
428
|
track.stop();
|
|
428
429
|
// Remove from PC if needed, or just stop sending
|
|
@@ -436,7 +437,7 @@ export class WebRtcManager {
|
|
|
436
437
|
});
|
|
437
438
|
this.#localStream = null;
|
|
438
439
|
this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, null);
|
|
439
|
-
this.#
|
|
440
|
+
this.#logDebug("Microphone disabled");
|
|
440
441
|
return true;
|
|
441
442
|
}
|
|
442
443
|
}
|
|
@@ -445,7 +446,7 @@ export class WebRtcManager {
|
|
|
445
446
|
* Transitions to DISCONNECTED state.
|
|
446
447
|
*/
|
|
447
448
|
disconnect() {
|
|
448
|
-
this.#
|
|
449
|
+
this.#logDebug("disconnect() called");
|
|
449
450
|
this.#cleanup();
|
|
450
451
|
this.#dispatch(WebRtcFsmEvent.DISCONNECT);
|
|
451
452
|
}
|
|
@@ -454,7 +455,7 @@ export class WebRtcManager {
|
|
|
454
455
|
* Cleans up all resources and allows reinitialization.
|
|
455
456
|
*/
|
|
456
457
|
reset() {
|
|
457
|
-
this.#
|
|
458
|
+
this.#logDebug("reset() called, current state:", this.state);
|
|
458
459
|
this.#cleanup();
|
|
459
460
|
// Reset from any non-IDLE state
|
|
460
461
|
if (this.state !== WebRtcState.IDLE) {
|
|
@@ -470,7 +471,7 @@ export class WebRtcManager {
|
|
|
470
471
|
this.#dispatch(WebRtcFsmEvent.RESET);
|
|
471
472
|
}
|
|
472
473
|
}
|
|
473
|
-
this.#
|
|
474
|
+
this.#logDebug("Reset complete, state:", this.state);
|
|
474
475
|
}
|
|
475
476
|
/**
|
|
476
477
|
* Creates a new data channel with the specified label.
|
|
@@ -480,24 +481,25 @@ export class WebRtcManager {
|
|
|
480
481
|
* @returns The created data channel, or null if peer connection not initialized.
|
|
481
482
|
*/
|
|
482
483
|
createDataChannel(label, options) {
|
|
483
|
-
this.#
|
|
484
|
+
this.#logDebug("createDataChannel() called:", label);
|
|
484
485
|
if (!this.#pc) {
|
|
485
|
-
this.#
|
|
486
|
+
this.#logDebug("Cannot create data channel: peer connection not initialized");
|
|
486
487
|
return null;
|
|
487
488
|
}
|
|
488
489
|
if (this.#dataChannels.has(label)) {
|
|
489
|
-
this.#
|
|
490
|
+
this.#logDebug("Returning existing data channel:", label);
|
|
490
491
|
return this.#dataChannels.get(label);
|
|
491
492
|
}
|
|
492
493
|
try {
|
|
493
494
|
const dc = this.#pc.createDataChannel(label, options);
|
|
494
495
|
this.#setupDataChannelListeners(dc);
|
|
495
496
|
this.#dataChannels.set(label, dc);
|
|
496
|
-
this.#
|
|
497
|
+
this.#logDebug("Data channel created:", label);
|
|
497
498
|
return dc;
|
|
498
499
|
}
|
|
499
500
|
catch (e) {
|
|
500
|
-
this.#
|
|
501
|
+
this.#logError(e);
|
|
502
|
+
this.#handleError(e);
|
|
501
503
|
return null;
|
|
502
504
|
}
|
|
503
505
|
}
|
|
@@ -519,11 +521,11 @@ export class WebRtcManager {
|
|
|
519
521
|
sendData(label, data) {
|
|
520
522
|
const channel = this.#dataChannels.get(label);
|
|
521
523
|
if (!channel) {
|
|
522
|
-
this.#
|
|
524
|
+
this.#logDebug(`Data channel '${label}' not found`);
|
|
523
525
|
return false;
|
|
524
526
|
}
|
|
525
527
|
if (channel.readyState !== "open") {
|
|
526
|
-
this.#
|
|
528
|
+
this.#logDebug(`Data channel '${label}' is not open (state: ${channel.readyState})`);
|
|
527
529
|
return false;
|
|
528
530
|
}
|
|
529
531
|
try {
|
|
@@ -532,7 +534,8 @@ export class WebRtcManager {
|
|
|
532
534
|
return true;
|
|
533
535
|
}
|
|
534
536
|
catch (e) {
|
|
535
|
-
this.#
|
|
537
|
+
this.#logError(e);
|
|
538
|
+
this.#handleError(e);
|
|
536
539
|
return false;
|
|
537
540
|
}
|
|
538
541
|
}
|
|
@@ -543,18 +546,19 @@ export class WebRtcManager {
|
|
|
543
546
|
* @returns The offer SDP, or null if peer connection not initialized.
|
|
544
547
|
*/
|
|
545
548
|
async createOffer(options) {
|
|
546
|
-
this.#
|
|
549
|
+
this.#logDebug("createOffer() called");
|
|
547
550
|
if (!this.#pc) {
|
|
548
|
-
this.#
|
|
551
|
+
this.#logDebug("Cannot create offer: peer connection not initialized");
|
|
549
552
|
return null;
|
|
550
553
|
}
|
|
551
554
|
try {
|
|
552
555
|
const offer = await this.#pc.createOffer(options);
|
|
553
|
-
this.#
|
|
556
|
+
this.#logDebug("Offer created:", offer.type);
|
|
554
557
|
return offer;
|
|
555
558
|
}
|
|
556
559
|
catch (e) {
|
|
557
|
-
this.#
|
|
560
|
+
this.#logError(e);
|
|
561
|
+
this.#handleError(e);
|
|
558
562
|
return null;
|
|
559
563
|
}
|
|
560
564
|
}
|
|
@@ -564,18 +568,19 @@ export class WebRtcManager {
|
|
|
564
568
|
* @returns The answer SDP, or null if peer connection not initialized.
|
|
565
569
|
*/
|
|
566
570
|
async createAnswer(options) {
|
|
567
|
-
this.#
|
|
571
|
+
this.#logDebug("createAnswer() called");
|
|
568
572
|
if (!this.#pc) {
|
|
569
|
-
this.#
|
|
573
|
+
this.#logDebug("Cannot create answer: peer connection not initialized");
|
|
570
574
|
return null;
|
|
571
575
|
}
|
|
572
576
|
try {
|
|
573
577
|
const answer = await this.#pc.createAnswer(options);
|
|
574
|
-
this.#
|
|
578
|
+
this.#logDebug("Answer created:", answer.type);
|
|
575
579
|
return answer;
|
|
576
580
|
}
|
|
577
581
|
catch (e) {
|
|
578
|
-
this.#
|
|
582
|
+
this.#logError(e);
|
|
583
|
+
this.#handleError(e);
|
|
579
584
|
return null;
|
|
580
585
|
}
|
|
581
586
|
}
|
|
@@ -585,18 +590,19 @@ export class WebRtcManager {
|
|
|
585
590
|
* @returns True if successful, false otherwise.
|
|
586
591
|
*/
|
|
587
592
|
async setLocalDescription(description) {
|
|
588
|
-
this.#
|
|
593
|
+
this.#logDebug("setLocalDescription() called:", description.type);
|
|
589
594
|
if (!this.#pc) {
|
|
590
|
-
this.#
|
|
595
|
+
this.#logDebug("Cannot set local description: peer connection not initialized");
|
|
591
596
|
return false;
|
|
592
597
|
}
|
|
593
598
|
try {
|
|
594
599
|
await this.#pc.setLocalDescription(description);
|
|
595
|
-
this.#
|
|
600
|
+
this.#logDebug("Local description set successfully");
|
|
596
601
|
return true;
|
|
597
602
|
}
|
|
598
603
|
catch (e) {
|
|
599
|
-
this.#
|
|
604
|
+
this.#logError(e);
|
|
605
|
+
this.#handleError(e);
|
|
600
606
|
return false;
|
|
601
607
|
}
|
|
602
608
|
}
|
|
@@ -606,18 +612,19 @@ export class WebRtcManager {
|
|
|
606
612
|
* @returns True if successful, false otherwise.
|
|
607
613
|
*/
|
|
608
614
|
async setRemoteDescription(description) {
|
|
609
|
-
this.#
|
|
615
|
+
this.#logDebug("setRemoteDescription() called:", description.type);
|
|
610
616
|
if (!this.#pc) {
|
|
611
|
-
this.#
|
|
617
|
+
this.#logDebug("Cannot set remote description: peer connection not initialized");
|
|
612
618
|
return false;
|
|
613
619
|
}
|
|
614
620
|
try {
|
|
615
621
|
await this.#pc.setRemoteDescription(description);
|
|
616
|
-
this.#
|
|
622
|
+
this.#logDebug("Remote description set successfully");
|
|
617
623
|
return true;
|
|
618
624
|
}
|
|
619
625
|
catch (e) {
|
|
620
|
-
this.#
|
|
626
|
+
this.#logError(e);
|
|
627
|
+
this.#handleError(e);
|
|
621
628
|
return false;
|
|
622
629
|
}
|
|
623
630
|
}
|
|
@@ -627,20 +634,21 @@ export class WebRtcManager {
|
|
|
627
634
|
* @returns True if successful, false otherwise.
|
|
628
635
|
*/
|
|
629
636
|
async addIceCandidate(candidate) {
|
|
630
|
-
this.#
|
|
637
|
+
this.#logDebug("addIceCandidate() called:", candidate ? "candidate" : "null (end-of-candidates)");
|
|
631
638
|
if (!this.#pc) {
|
|
632
|
-
this.#
|
|
639
|
+
this.#logDebug("Cannot add ICE candidate: peer connection not initialized");
|
|
633
640
|
return false;
|
|
634
641
|
}
|
|
635
642
|
try {
|
|
636
643
|
if (candidate) {
|
|
637
644
|
await this.#pc.addIceCandidate(candidate);
|
|
638
|
-
this.#
|
|
645
|
+
this.#logDebug("ICE candidate added");
|
|
639
646
|
}
|
|
640
647
|
return true;
|
|
641
648
|
}
|
|
642
649
|
catch (e) {
|
|
643
|
-
this.#
|
|
650
|
+
this.#logError(e);
|
|
651
|
+
this.#handleError(e);
|
|
644
652
|
return false;
|
|
645
653
|
}
|
|
646
654
|
}
|
|
@@ -650,19 +658,20 @@ export class WebRtcManager {
|
|
|
650
658
|
* @returns True if successful, false otherwise.
|
|
651
659
|
*/
|
|
652
660
|
async iceRestart() {
|
|
653
|
-
this.#
|
|
661
|
+
this.#logDebug("iceRestart() called");
|
|
654
662
|
if (!this.#pc) {
|
|
655
|
-
this.#
|
|
663
|
+
this.#logDebug("Cannot perform ICE restart: peer connection not initialized");
|
|
656
664
|
return false;
|
|
657
665
|
}
|
|
658
666
|
try {
|
|
659
667
|
const offer = await this.#pc.createOffer({ iceRestart: true });
|
|
660
668
|
await this.#pc.setLocalDescription(offer);
|
|
661
|
-
this.#
|
|
669
|
+
this.#logDebug("ICE restart initiated");
|
|
662
670
|
return true;
|
|
663
671
|
}
|
|
664
672
|
catch (e) {
|
|
665
|
-
this.#
|
|
673
|
+
this.#logError(e);
|
|
674
|
+
this.#handleError(e);
|
|
666
675
|
return false;
|
|
667
676
|
}
|
|
668
677
|
}
|
|
@@ -691,7 +700,8 @@ export class WebRtcManager {
|
|
|
691
700
|
return await this.#pc.getStats();
|
|
692
701
|
}
|
|
693
702
|
catch (e) {
|
|
694
|
-
this.#
|
|
703
|
+
this.#logError(e);
|
|
704
|
+
this.#handleError(e);
|
|
695
705
|
return null;
|
|
696
706
|
}
|
|
697
707
|
}
|
|
@@ -701,12 +711,12 @@ export class WebRtcManager {
|
|
|
701
711
|
this.#fsm.transition(event);
|
|
702
712
|
const newState = this.#fsm.state;
|
|
703
713
|
if (oldState !== newState) {
|
|
704
|
-
this.#
|
|
714
|
+
this.#logDebug("State transition:", oldState, "->", newState, "(event:", event + ")");
|
|
705
715
|
this.#pubsub.publish(WebRtcManager.EVENT_STATE_CHANGE, newState);
|
|
706
716
|
}
|
|
707
717
|
}
|
|
708
718
|
// deno-lint-ignore no-explicit-any
|
|
709
|
-
#
|
|
719
|
+
#logDebug(...args) {
|
|
710
720
|
if (this.#config.debug) {
|
|
711
721
|
this.#logger.debug("[WebRtcManager]", ...args);
|
|
712
722
|
}
|
|
@@ -716,22 +726,25 @@ export class WebRtcManager {
|
|
|
716
726
|
this.#logger.log("[WebRtcManager]", ...args);
|
|
717
727
|
}
|
|
718
728
|
// deno-lint-ignore no-explicit-any
|
|
719
|
-
#
|
|
729
|
+
#logWarn(...args) {
|
|
720
730
|
this.#logger.warn("[WebRtcManager]", ...args);
|
|
721
731
|
}
|
|
722
732
|
// deno-lint-ignore no-explicit-any
|
|
723
|
-
#
|
|
724
|
-
this.#logger.error("[WebRtcManager]",
|
|
733
|
+
#logError(...args) {
|
|
734
|
+
this.#logger.error("[WebRtcManager]", ...args);
|
|
735
|
+
}
|
|
736
|
+
// deno-lint-ignore no-explicit-any
|
|
737
|
+
#handleError(error) {
|
|
725
738
|
this.#dispatch(WebRtcFsmEvent.ERROR);
|
|
726
739
|
this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
|
|
727
740
|
}
|
|
728
741
|
#setupPcListeners() {
|
|
729
742
|
if (!this.#pc)
|
|
730
743
|
return;
|
|
731
|
-
this.#
|
|
744
|
+
this.#logDebug("Setting up peer connection listeners");
|
|
732
745
|
this.#pc.onconnectionstatechange = () => {
|
|
733
746
|
const state = this.#pc.connectionState;
|
|
734
|
-
this.#
|
|
747
|
+
this.#logDebug("Connection state changed:", state);
|
|
735
748
|
if (state === "connected") {
|
|
736
749
|
// Connection successful - reset reconnect attempts and clear any pending timeout
|
|
737
750
|
this.#reconnectAttempts = 0;
|
|
@@ -755,7 +768,7 @@ export class WebRtcManager {
|
|
|
755
768
|
}
|
|
756
769
|
};
|
|
757
770
|
this.#pc.ontrack = (event) => {
|
|
758
|
-
this.#
|
|
771
|
+
this.#logDebug("Remote track received:", event.track.kind);
|
|
759
772
|
if (event.streams && event.streams[0]) {
|
|
760
773
|
this.#remoteStream = event.streams[0];
|
|
761
774
|
this.#pubsub.publish(WebRtcManager.EVENT_REMOTE_STREAM, this.#remoteStream);
|
|
@@ -763,17 +776,17 @@ export class WebRtcManager {
|
|
|
763
776
|
};
|
|
764
777
|
this.#pc.ondatachannel = (event) => {
|
|
765
778
|
const dc = event.channel;
|
|
766
|
-
this.#
|
|
779
|
+
this.#logDebug("Remote data channel received:", dc.label);
|
|
767
780
|
this.#setupDataChannelListeners(dc);
|
|
768
781
|
this.#dataChannels.set(dc.label, dc);
|
|
769
782
|
};
|
|
770
783
|
this.#pc.onicecandidate = (event) => {
|
|
771
|
-
this.#
|
|
784
|
+
this.#logDebug("ICE candidate generated:", event.candidate ? "candidate" : "null (gathering complete)");
|
|
772
785
|
this.#pubsub.publish(WebRtcManager.EVENT_ICE_CANDIDATE, event.candidate);
|
|
773
786
|
};
|
|
774
787
|
}
|
|
775
788
|
#cleanup() {
|
|
776
|
-
this.#
|
|
789
|
+
this.#logDebug("Cleanup started");
|
|
777
790
|
// Clear any pending reconnect timers
|
|
778
791
|
if (this.#reconnectTimer !== null) {
|
|
779
792
|
clearTimeout(this.#reconnectTimer);
|
|
@@ -798,25 +811,25 @@ export class WebRtcManager {
|
|
|
798
811
|
});
|
|
799
812
|
this.#dataChannels.clear();
|
|
800
813
|
if (dcCount > 0) {
|
|
801
|
-
this.#
|
|
814
|
+
this.#logDebug("Closed", dcCount, "data channel(s)");
|
|
802
815
|
}
|
|
803
816
|
// Stop local stream tracks
|
|
804
817
|
if (this.#localStream) {
|
|
805
818
|
this.#localStream.getTracks().forEach((track) => track.stop());
|
|
806
819
|
this.#localStream = null;
|
|
807
|
-
this.#
|
|
820
|
+
this.#logDebug("Local stream stopped");
|
|
808
821
|
}
|
|
809
822
|
// Close peer connection
|
|
810
823
|
if (this.#pc) {
|
|
811
824
|
this.#pc.close();
|
|
812
825
|
this.#pc = null;
|
|
813
|
-
this.#
|
|
826
|
+
this.#logDebug("Peer connection closed");
|
|
814
827
|
}
|
|
815
828
|
this.#remoteStream = null;
|
|
816
|
-
this.#
|
|
829
|
+
this.#logDebug("Cleanup complete");
|
|
817
830
|
}
|
|
818
831
|
#handleConnectionFailure() {
|
|
819
|
-
this.#
|
|
832
|
+
this.#logDebug("Handling connection failure");
|
|
820
833
|
// Only dispatch DISCONNECT if not already in a terminal state
|
|
821
834
|
if (this.state !== WebRtcState.DISCONNECTED &&
|
|
822
835
|
this.state !== WebRtcState.ERROR &&
|
|
@@ -825,13 +838,13 @@ export class WebRtcManager {
|
|
|
825
838
|
}
|
|
826
839
|
// Check if auto-reconnect is enabled
|
|
827
840
|
if (!this.#config.autoReconnect) {
|
|
828
|
-
this.#
|
|
841
|
+
this.#logDebug("Auto-reconnect disabled, not attempting reconnection");
|
|
829
842
|
return;
|
|
830
843
|
}
|
|
831
844
|
const maxAttempts = this.#config.maxReconnectAttempts ?? 5;
|
|
832
845
|
// Check if we've exceeded max attempts
|
|
833
846
|
if (this.#reconnectAttempts >= maxAttempts) {
|
|
834
|
-
this.#
|
|
847
|
+
this.#logDebug("Max reconnection attempts reached:", maxAttempts);
|
|
835
848
|
this.#pubsub.publish(WebRtcManager.EVENT_RECONNECT_FAILED, {
|
|
836
849
|
attempts: this.#reconnectAttempts,
|
|
837
850
|
});
|
|
@@ -848,7 +861,7 @@ export class WebRtcManager {
|
|
|
848
861
|
strategy,
|
|
849
862
|
});
|
|
850
863
|
if (!shouldProceed) {
|
|
851
|
-
this.#
|
|
864
|
+
this.#logDebug("Reconnection suppressed by shouldReconnect callback");
|
|
852
865
|
return;
|
|
853
866
|
}
|
|
854
867
|
}
|
|
@@ -863,7 +876,7 @@ export class WebRtcManager {
|
|
|
863
876
|
const delay = baseDelay * Math.pow(2, this.#reconnectAttempts - 1);
|
|
864
877
|
// Try ICE restart first (attempts 1-2), then full reconnect
|
|
865
878
|
const strategy = this.#reconnectAttempts <= 2 ? "ice-restart" : "full";
|
|
866
|
-
this.#
|
|
879
|
+
this.#logDebug("Attempting reconnection:", {
|
|
867
880
|
attempt: this.#reconnectAttempts,
|
|
868
881
|
strategy,
|
|
869
882
|
delay: delay + "ms",
|
|
@@ -901,13 +914,13 @@ export class WebRtcManager {
|
|
|
901
914
|
this.#fullReconnectTimeoutTimer = null;
|
|
902
915
|
// Only trigger failure if still not connected
|
|
903
916
|
if (this.state !== WebRtcState.CONNECTED) {
|
|
904
|
-
this.#
|
|
917
|
+
this.#logDebug("Full reconnection timeout reached, connection not established");
|
|
905
918
|
this.#handleConnectionFailure();
|
|
906
919
|
}
|
|
907
920
|
}, timeout);
|
|
908
921
|
}
|
|
909
922
|
catch (e) {
|
|
910
|
-
this.#
|
|
923
|
+
this.#logError("Reconnection failed:", e);
|
|
911
924
|
this.#handleConnectionFailure();
|
|
912
925
|
}
|
|
913
926
|
}
|
|
@@ -928,7 +941,7 @@ export class WebRtcManager {
|
|
|
928
941
|
this.#pubsub.publish(WebRtcManager.EVENT_DEVICE_CHANGED, devices);
|
|
929
942
|
}
|
|
930
943
|
catch (e) {
|
|
931
|
-
this.#
|
|
944
|
+
this.#logError("Error handling device change:", e);
|
|
932
945
|
}
|
|
933
946
|
};
|
|
934
947
|
navigator.mediaDevices.addEventListener("devicechange", this.#deviceChangeHandler);
|
|
@@ -952,7 +965,7 @@ export class WebRtcManager {
|
|
|
952
965
|
// Ignore "User-Initiated Abort" errors which occur during intentional close()
|
|
953
966
|
const isUserAbort = error?.error?.message?.includes("User-Initiated Abort");
|
|
954
967
|
if (!isUserAbort) {
|
|
955
|
-
this.#
|
|
968
|
+
this.#logError("Data channel error:", error);
|
|
956
969
|
this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
|
|
957
970
|
}
|
|
958
971
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/webrtc",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/mod.js",
|
|
6
6
|
"types": "dist/mod.d.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"author": "Marian Meres",
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@marianmeres/fsm": "^2.11.
|
|
16
|
+
"@marianmeres/fsm": "^2.11.1",
|
|
17
17
|
"@marianmeres/pubsub": "^2.4.4"
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|