@marianmeres/webrtc 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +22 -6
- package/API.md +33 -0
- package/README.md +1 -0
- package/dist/types.d.ts +73 -1
- package/dist/types.js +22 -0
- package/dist/webrtc-manager.js +150 -25
- package/package.json +9 -3
- package/llm.txt +0 -364
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
```yaml
|
|
6
6
|
name: "@marianmeres/webrtc"
|
|
7
|
-
version: "
|
|
7
|
+
version: "1.0.2"
|
|
8
8
|
license: MIT
|
|
9
9
|
author: Marian Meres
|
|
10
10
|
repository: https://github.com/marianmeres/webrtc
|
|
@@ -27,12 +27,12 @@ A lightweight, framework-agnostic WebRTC manager providing:
|
|
|
27
27
|
|
|
28
28
|
```yaml
|
|
29
29
|
production:
|
|
30
|
-
- "@marianmeres/fsm": "^2.
|
|
31
|
-
- "@marianmeres/pubsub": "^2.4.
|
|
30
|
+
- "@marianmeres/fsm": "^2.11.0"
|
|
31
|
+
- "@marianmeres/pubsub": "^2.4.4"
|
|
32
32
|
development:
|
|
33
|
-
- "@std/assert":
|
|
34
|
-
- "@std/fs":
|
|
35
|
-
- "@std/path":
|
|
33
|
+
- "@std/assert": "^1.0.16"
|
|
34
|
+
- "@std/fs": "^1.0.20"
|
|
35
|
+
- "@std/path": "^1.1.3"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## File Structure
|
|
@@ -117,6 +117,21 @@ ERROR --RESET--> IDLE
|
|
|
117
117
|
new WebRtcManager(factory: WebRtcFactory, config?: WebRtcManagerConfig)
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
### Logger Interface
|
|
121
|
+
|
|
122
|
+
Console-compatible logger interface for custom logging implementations.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
interface Logger {
|
|
126
|
+
debug: (...args: any[]) => any;
|
|
127
|
+
log: (...args: any[]) => any;
|
|
128
|
+
warn: (...args: any[]) => any;
|
|
129
|
+
error: (...args: any[]) => any;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Each method returns a string representation of the first argument, enabling patterns like `throw new Error(logger.error("msg"))`.
|
|
134
|
+
|
|
120
135
|
### WebRtcFactory Interface
|
|
121
136
|
|
|
122
137
|
```typescript
|
|
@@ -138,6 +153,7 @@ interface WebRtcManagerConfig {
|
|
|
138
153
|
maxReconnectAttempts?: number; // Default: 5
|
|
139
154
|
reconnectDelay?: number; // Default: 1000ms
|
|
140
155
|
debug?: boolean; // Default: false
|
|
156
|
+
logger?: Logger; // Custom logger, falls back to console
|
|
141
157
|
}
|
|
142
158
|
```
|
|
143
159
|
|
package/API.md
CHANGED
|
@@ -681,6 +681,36 @@ console.log(manager.toMermaid());
|
|
|
681
681
|
|
|
682
682
|
## Types
|
|
683
683
|
|
|
684
|
+
### Logger
|
|
685
|
+
|
|
686
|
+
Console-compatible logger interface for custom logging implementations.
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
interface Logger {
|
|
690
|
+
debug: (...args: any[]) => any;
|
|
691
|
+
log: (...args: any[]) => any;
|
|
692
|
+
warn: (...args: any[]) => any;
|
|
693
|
+
error: (...args: any[]) => any;
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Each method accepts variadic arguments and returns a string representation of the first argument. This enables patterns like `throw new Error(logger.error("msg"))`.
|
|
698
|
+
|
|
699
|
+
**Example Custom Logger:**
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
import { clog } from '@marianmeres/clog';
|
|
703
|
+
|
|
704
|
+
const logger = clog('WebRTC');
|
|
705
|
+
|
|
706
|
+
const manager = new WebRtcManager(factory, {
|
|
707
|
+
debug: true,
|
|
708
|
+
logger: logger,
|
|
709
|
+
});
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
684
714
|
### WebRtcFactory
|
|
685
715
|
|
|
686
716
|
Interface for dependency injection of WebRTC primitives.
|
|
@@ -731,6 +761,9 @@ interface WebRtcManagerConfig {
|
|
|
731
761
|
|
|
732
762
|
/** Enable debug logging. Default: false */
|
|
733
763
|
debug?: boolean;
|
|
764
|
+
|
|
765
|
+
/** Custom logger instance. If not provided, falls back to console. */
|
|
766
|
+
logger?: Logger;
|
|
734
767
|
}
|
|
735
768
|
```
|
|
736
769
|
|
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ const manager = new WebRtcManager(factory, config);
|
|
|
52
52
|
- `maxReconnectAttempts`: Max reconnection attempts (default: 5)
|
|
53
53
|
- `reconnectDelay`: Initial reconnection delay in ms (default: 1000)
|
|
54
54
|
- `debug`: Enable debug logging (default: false)
|
|
55
|
+
- `logger`: Custom logger instance implementing `Logger` interface (default: console)
|
|
55
56
|
|
|
56
57
|
### State and Properties
|
|
57
58
|
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console-compatible logger interface (see @marianmeres/clog ).
|
|
3
|
+
* Each method accepts variadic arguments and returns a string representation of the first argument.
|
|
4
|
+
* This enables patterns like `throw new Error(logger.error("msg"))`.
|
|
5
|
+
*/
|
|
6
|
+
export interface Logger {
|
|
7
|
+
debug: (...args: any[]) => any;
|
|
8
|
+
log: (...args: any[]) => any;
|
|
9
|
+
warn: (...args: any[]) => any;
|
|
10
|
+
error: (...args: any[]) => any;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Configuration options for WebRtcManager.
|
|
14
|
+
* All options are optional with sensible defaults.
|
|
15
|
+
*/
|
|
1
16
|
export interface WebRtcManagerConfig {
|
|
2
17
|
/** Initial peer configuration (ICE servers, etc.) */
|
|
3
18
|
peerConfig?: RTCConfiguration;
|
|
@@ -11,42 +26,95 @@ export interface WebRtcManagerConfig {
|
|
|
11
26
|
maxReconnectAttempts?: number;
|
|
12
27
|
/** Initial reconnection delay in ms. Doubles with each attempt. Defaults to 1000. */
|
|
13
28
|
reconnectDelay?: number;
|
|
14
|
-
/**
|
|
29
|
+
/** Enable debug logging. Defaults to false. */
|
|
15
30
|
debug?: boolean;
|
|
31
|
+
/** Custom logger instance. If not provided, falls back to console. */
|
|
32
|
+
logger?: Logger;
|
|
16
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Factory interface for creating WebRTC primitives.
|
|
36
|
+
* Allows dependency injection of browser APIs for testing and flexibility.
|
|
37
|
+
*/
|
|
17
38
|
export interface WebRtcFactory {
|
|
39
|
+
/**
|
|
40
|
+
* Creates a new RTCPeerConnection instance.
|
|
41
|
+
* @param config - Optional RTCConfiguration for ICE servers, certificates, etc.
|
|
42
|
+
* @returns A new RTCPeerConnection instance.
|
|
43
|
+
*/
|
|
18
44
|
createPeerConnection(config?: RTCConfiguration): RTCPeerConnection;
|
|
45
|
+
/**
|
|
46
|
+
* Requests access to user media (microphone/camera).
|
|
47
|
+
* @param constraints - Media constraints specifying audio/video requirements.
|
|
48
|
+
* @returns Promise resolving to a MediaStream.
|
|
49
|
+
*/
|
|
19
50
|
getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
|
51
|
+
/**
|
|
52
|
+
* Enumerates all available media input and output devices.
|
|
53
|
+
* @returns Promise resolving to an array of MediaDeviceInfo objects.
|
|
54
|
+
*/
|
|
20
55
|
enumerateDevices(): Promise<MediaDeviceInfo[]>;
|
|
21
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Possible states of the WebRTC connection lifecycle.
|
|
59
|
+
* The manager transitions between these states based on connection events.
|
|
60
|
+
*/
|
|
22
61
|
export declare enum WebRtcState {
|
|
62
|
+
/** Initial state, no resources allocated. */
|
|
23
63
|
IDLE = "IDLE",
|
|
64
|
+
/** Creating peer connection and setting up media tracks. */
|
|
24
65
|
INITIALIZING = "INITIALIZING",
|
|
66
|
+
/** Performing SDP offer/answer exchange. */
|
|
25
67
|
CONNECTING = "CONNECTING",
|
|
68
|
+
/** Connection established, communication active. */
|
|
26
69
|
CONNECTED = "CONNECTED",
|
|
70
|
+
/** Automatic reconnection in progress. */
|
|
27
71
|
RECONNECTING = "RECONNECTING",
|
|
72
|
+
/** Connection closed, can be restarted. */
|
|
28
73
|
DISCONNECTED = "DISCONNECTED",
|
|
74
|
+
/** Error state, requires reset() to recover. */
|
|
29
75
|
ERROR = "ERROR"
|
|
30
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Internal FSM events that trigger state transitions.
|
|
79
|
+
* These events are dispatched internally by the manager methods.
|
|
80
|
+
*/
|
|
31
81
|
export declare enum WebRtcFsmEvent {
|
|
82
|
+
/** Triggers transition from IDLE to INITIALIZING. */
|
|
32
83
|
INIT = "initialize",
|
|
84
|
+
/** Triggers transition to CONNECTING state. */
|
|
33
85
|
CONNECT = "connect",
|
|
86
|
+
/** Signals successful connection establishment. */
|
|
34
87
|
CONNECTED = "connected",
|
|
88
|
+
/** Triggers transition to RECONNECTING state. */
|
|
35
89
|
RECONNECTING = "reconnecting",
|
|
90
|
+
/** Triggers transition to DISCONNECTED state. */
|
|
36
91
|
DISCONNECT = "disconnect",
|
|
92
|
+
/** Triggers transition to ERROR state. */
|
|
37
93
|
ERROR = "error",
|
|
94
|
+
/** Resets the manager to IDLE state. */
|
|
38
95
|
RESET = "reset"
|
|
39
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Type definitions for all WebRTC manager events and their payloads.
|
|
99
|
+
* Use with the `on()` method to subscribe to specific events.
|
|
100
|
+
*/
|
|
40
101
|
export interface WebRtcEvents {
|
|
102
|
+
/** Emitted when connection state changes. Payload: the new WebRtcState. */
|
|
41
103
|
state_change: WebRtcState;
|
|
104
|
+
/** Emitted when local media stream changes. Payload: MediaStream or null if disabled. */
|
|
42
105
|
local_stream: MediaStream | null;
|
|
106
|
+
/** Emitted when remote media stream is received. Payload: MediaStream or null. */
|
|
43
107
|
remote_stream: MediaStream | null;
|
|
108
|
+
/** Emitted when a data channel opens. Payload: the RTCDataChannel. */
|
|
44
109
|
data_channel_open: RTCDataChannel;
|
|
110
|
+
/** Emitted when a data channel receives a message. Payload: channel and data. */
|
|
45
111
|
data_channel_message: {
|
|
46
112
|
channel: RTCDataChannel;
|
|
47
113
|
data: any;
|
|
48
114
|
};
|
|
115
|
+
/** Emitted when a data channel closes. Payload: the RTCDataChannel. */
|
|
49
116
|
data_channel_close: RTCDataChannel;
|
|
117
|
+
/** Emitted when an ICE candidate is generated. Payload: RTCIceCandidate or null. */
|
|
50
118
|
ice_candidate: RTCIceCandidate | null;
|
|
51
119
|
/**
|
|
52
120
|
* Emitted when reconnection is being attempted.
|
|
@@ -58,13 +126,17 @@ export interface WebRtcEvents {
|
|
|
58
126
|
attempt: number;
|
|
59
127
|
strategy: "ice-restart" | "full";
|
|
60
128
|
};
|
|
129
|
+
/** Emitted when all reconnection attempts have failed. Payload: total attempts. */
|
|
61
130
|
reconnect_failed: {
|
|
62
131
|
attempts: number;
|
|
63
132
|
};
|
|
133
|
+
/** Emitted when audio devices change. Payload: array of available audio inputs. */
|
|
64
134
|
device_changed: MediaDeviceInfo[];
|
|
135
|
+
/** Emitted when microphone access or switching fails. */
|
|
65
136
|
microphone_failed: {
|
|
66
137
|
error?: any;
|
|
67
138
|
reason?: string;
|
|
68
139
|
};
|
|
140
|
+
/** Emitted when an error occurs. Payload: the Error object. */
|
|
69
141
|
error: Error;
|
|
70
142
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Possible states of the WebRTC connection lifecycle.
|
|
3
|
+
* The manager transitions between these states based on connection events.
|
|
4
|
+
*/
|
|
1
5
|
export var WebRtcState;
|
|
2
6
|
(function (WebRtcState) {
|
|
7
|
+
/** Initial state, no resources allocated. */
|
|
3
8
|
WebRtcState["IDLE"] = "IDLE";
|
|
9
|
+
/** Creating peer connection and setting up media tracks. */
|
|
4
10
|
WebRtcState["INITIALIZING"] = "INITIALIZING";
|
|
11
|
+
/** Performing SDP offer/answer exchange. */
|
|
5
12
|
WebRtcState["CONNECTING"] = "CONNECTING";
|
|
13
|
+
/** Connection established, communication active. */
|
|
6
14
|
WebRtcState["CONNECTED"] = "CONNECTED";
|
|
15
|
+
/** Automatic reconnection in progress. */
|
|
7
16
|
WebRtcState["RECONNECTING"] = "RECONNECTING";
|
|
17
|
+
/** Connection closed, can be restarted. */
|
|
8
18
|
WebRtcState["DISCONNECTED"] = "DISCONNECTED";
|
|
19
|
+
/** Error state, requires reset() to recover. */
|
|
9
20
|
WebRtcState["ERROR"] = "ERROR";
|
|
10
21
|
})(WebRtcState || (WebRtcState = {}));
|
|
22
|
+
/**
|
|
23
|
+
* Internal FSM events that trigger state transitions.
|
|
24
|
+
* These events are dispatched internally by the manager methods.
|
|
25
|
+
*/
|
|
11
26
|
export var WebRtcFsmEvent;
|
|
12
27
|
(function (WebRtcFsmEvent) {
|
|
28
|
+
/** Triggers transition from IDLE to INITIALIZING. */
|
|
13
29
|
WebRtcFsmEvent["INIT"] = "initialize";
|
|
30
|
+
/** Triggers transition to CONNECTING state. */
|
|
14
31
|
WebRtcFsmEvent["CONNECT"] = "connect";
|
|
32
|
+
/** Signals successful connection establishment. */
|
|
15
33
|
WebRtcFsmEvent["CONNECTED"] = "connected";
|
|
34
|
+
/** Triggers transition to RECONNECTING state. */
|
|
16
35
|
WebRtcFsmEvent["RECONNECTING"] = "reconnecting";
|
|
36
|
+
/** Triggers transition to DISCONNECTED state. */
|
|
17
37
|
WebRtcFsmEvent["DISCONNECT"] = "disconnect";
|
|
38
|
+
/** Triggers transition to ERROR state. */
|
|
18
39
|
WebRtcFsmEvent["ERROR"] = "error";
|
|
40
|
+
/** Resets the manager to IDLE state. */
|
|
19
41
|
WebRtcFsmEvent["RESET"] = "reset";
|
|
20
42
|
})(WebRtcFsmEvent || (WebRtcFsmEvent = {}));
|
package/dist/webrtc-manager.js
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { FSM } from "@marianmeres/fsm";
|
|
2
2
|
import { PubSub } from "@marianmeres/pubsub";
|
|
3
3
|
import { WebRtcState, WebRtcFsmEvent, } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Default console-based logger that wraps console methods.
|
|
6
|
+
* Returns string representation of the first argument for chaining.
|
|
7
|
+
*/
|
|
8
|
+
const createDefaultLogger = () => ({
|
|
9
|
+
// deno-lint-ignore no-explicit-any
|
|
10
|
+
debug: (...args) => {
|
|
11
|
+
console.debug(...args);
|
|
12
|
+
return String(args[0] ?? "");
|
|
13
|
+
},
|
|
14
|
+
// deno-lint-ignore no-explicit-any
|
|
15
|
+
log: (...args) => {
|
|
16
|
+
console.log(...args);
|
|
17
|
+
return String(args[0] ?? "");
|
|
18
|
+
},
|
|
19
|
+
// deno-lint-ignore no-explicit-any
|
|
20
|
+
warn: (...args) => {
|
|
21
|
+
console.warn(...args);
|
|
22
|
+
return String(args[0] ?? "");
|
|
23
|
+
},
|
|
24
|
+
// deno-lint-ignore no-explicit-any
|
|
25
|
+
error: (...args) => {
|
|
26
|
+
console.error(...args);
|
|
27
|
+
return String(args[0] ?? "");
|
|
28
|
+
},
|
|
29
|
+
});
|
|
4
30
|
/**
|
|
5
31
|
* WebRTC connection manager with FSM-based lifecycle and event-driven architecture.
|
|
6
32
|
*
|
|
@@ -57,6 +83,7 @@ export class WebRtcManager {
|
|
|
57
83
|
#pc = null;
|
|
58
84
|
#factory;
|
|
59
85
|
#config;
|
|
86
|
+
#logger;
|
|
60
87
|
#localStream = null;
|
|
61
88
|
#remoteStream = null;
|
|
62
89
|
#dataChannels = new Map();
|
|
@@ -71,6 +98,7 @@ export class WebRtcManager {
|
|
|
71
98
|
constructor(factory, config = {}) {
|
|
72
99
|
this.#factory = factory;
|
|
73
100
|
this.#config = config;
|
|
101
|
+
this.#logger = config.logger ?? createDefaultLogger();
|
|
74
102
|
this.#pubsub = new PubSub();
|
|
75
103
|
// Initialize FSM
|
|
76
104
|
this.#fsm = new FSM({
|
|
@@ -149,6 +177,7 @@ export class WebRtcManager {
|
|
|
149
177
|
* @param handler - Callback function that receives the event data.
|
|
150
178
|
* @returns Unsubscribe function to remove the event listener.
|
|
151
179
|
*/
|
|
180
|
+
// deno-lint-ignore no-explicit-any
|
|
152
181
|
on(event, handler) {
|
|
153
182
|
return this.#pubsub.subscribe(event, handler);
|
|
154
183
|
}
|
|
@@ -193,7 +222,7 @@ export class WebRtcManager {
|
|
|
193
222
|
return devices.filter((d) => d.kind === "audioinput");
|
|
194
223
|
}
|
|
195
224
|
catch (e) {
|
|
196
|
-
|
|
225
|
+
this.#logger.error("[WebRtcManager] Failed to enumerate devices:", e);
|
|
197
226
|
return [];
|
|
198
227
|
}
|
|
199
228
|
}
|
|
@@ -204,7 +233,7 @@ export class WebRtcManager {
|
|
|
204
233
|
*/
|
|
205
234
|
async switchMicrophone(deviceId) {
|
|
206
235
|
if (!this.#pc || !this.#localStream) {
|
|
207
|
-
|
|
236
|
+
this.#logger.error("[WebRtcManager] Cannot switch microphone: not initialized or no active stream");
|
|
208
237
|
return false;
|
|
209
238
|
}
|
|
210
239
|
try {
|
|
@@ -240,7 +269,7 @@ export class WebRtcManager {
|
|
|
240
269
|
return true;
|
|
241
270
|
}
|
|
242
271
|
catch (e) {
|
|
243
|
-
|
|
272
|
+
this.#logger.error("[WebRtcManager] Failed to switch microphone:", e);
|
|
244
273
|
this.#error(e);
|
|
245
274
|
return false;
|
|
246
275
|
}
|
|
@@ -250,15 +279,20 @@ export class WebRtcManager {
|
|
|
250
279
|
* Must be called before creating offers or answers. Can only be called from IDLE state.
|
|
251
280
|
*/
|
|
252
281
|
async initialize() {
|
|
253
|
-
if (this.state !== WebRtcState.IDLE)
|
|
282
|
+
if (this.state !== WebRtcState.IDLE) {
|
|
283
|
+
this.#debug("initialize() called but state is not IDLE:", this.state);
|
|
254
284
|
return;
|
|
285
|
+
}
|
|
286
|
+
this.#debug("Initializing...");
|
|
255
287
|
this.#dispatch(WebRtcFsmEvent.INIT);
|
|
256
288
|
try {
|
|
257
289
|
this.#pc = this.#factory.createPeerConnection(this.#config.peerConfig);
|
|
290
|
+
this.#debug("Peer connection created");
|
|
258
291
|
this.#setupPcListeners();
|
|
259
292
|
// Setup device change detection now that we have a connection
|
|
260
293
|
this.#setupDeviceChangeListener();
|
|
261
294
|
if (this.#config.enableMicrophone) {
|
|
295
|
+
this.#debug("Enabling microphone (config enabled)");
|
|
262
296
|
const success = await this.enableMicrophone(true);
|
|
263
297
|
if (!success) {
|
|
264
298
|
this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
|
|
@@ -270,10 +304,13 @@ export class WebRtcManager {
|
|
|
270
304
|
// Always setup to receive audio, even if we don't enable microphone
|
|
271
305
|
// This ensures the SDP includes audio media line
|
|
272
306
|
this.#pc.addTransceiver("audio", { direction: "recvonly" });
|
|
307
|
+
this.#debug("Added recvonly audio transceiver");
|
|
273
308
|
}
|
|
274
309
|
if (this.#config.dataChannelLabel) {
|
|
310
|
+
this.#debug("Creating default data channel:", this.#config.dataChannelLabel);
|
|
275
311
|
this.createDataChannel(this.#config.dataChannelLabel);
|
|
276
312
|
}
|
|
313
|
+
this.#debug("Initialization complete");
|
|
277
314
|
}
|
|
278
315
|
catch (e) {
|
|
279
316
|
this.#error(e);
|
|
@@ -284,12 +321,15 @@ export class WebRtcManager {
|
|
|
284
321
|
* If disconnected, reinitializes the peer connection.
|
|
285
322
|
*/
|
|
286
323
|
async connect() {
|
|
324
|
+
this.#debug("connect() called, current state:", this.state);
|
|
287
325
|
// Initialize if needed
|
|
288
326
|
if (this.state === WebRtcState.IDLE) {
|
|
327
|
+
this.#debug("State is IDLE, initializing first");
|
|
289
328
|
await this.initialize();
|
|
290
329
|
}
|
|
291
330
|
// Reinitialize if disconnected (PeerConnection was closed)
|
|
292
331
|
if (this.state === WebRtcState.DISCONNECTED) {
|
|
332
|
+
this.#debug("State is DISCONNECTED, reinitializing");
|
|
293
333
|
// Clean up old connection
|
|
294
334
|
this.#cleanup();
|
|
295
335
|
// Reset to IDLE and reinitialize
|
|
@@ -299,8 +339,11 @@ export class WebRtcManager {
|
|
|
299
339
|
return;
|
|
300
340
|
}
|
|
301
341
|
if (this.state === WebRtcState.CONNECTED ||
|
|
302
|
-
this.state === WebRtcState.CONNECTING)
|
|
342
|
+
this.state === WebRtcState.CONNECTING) {
|
|
343
|
+
this.#debug("Already connected or connecting, skipping");
|
|
303
344
|
return;
|
|
345
|
+
}
|
|
346
|
+
this.#debug("Transitioning to CONNECTING");
|
|
304
347
|
this.#dispatch(WebRtcFsmEvent.CONNECT);
|
|
305
348
|
}
|
|
306
349
|
/**
|
|
@@ -309,14 +352,19 @@ export class WebRtcManager {
|
|
|
309
352
|
* @returns True if successful, false if failed to get user media.
|
|
310
353
|
*/
|
|
311
354
|
async enableMicrophone(enable) {
|
|
355
|
+
this.#debug("enableMicrophone() called:", enable);
|
|
312
356
|
if (enable) {
|
|
313
|
-
if (this.#localStream)
|
|
314
|
-
|
|
357
|
+
if (this.#localStream) {
|
|
358
|
+
this.#debug("Microphone already enabled");
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
315
361
|
try {
|
|
362
|
+
this.#debug("Requesting user media...");
|
|
316
363
|
const stream = await this.#factory.getUserMedia({
|
|
317
364
|
audio: true,
|
|
318
365
|
video: false,
|
|
319
366
|
});
|
|
367
|
+
this.#debug("User media obtained, tracks:", stream.getAudioTracks().length);
|
|
320
368
|
this.#localStream = stream;
|
|
321
369
|
this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, stream);
|
|
322
370
|
if (this.#pc) {
|
|
@@ -329,25 +377,33 @@ export class WebRtcManager {
|
|
|
329
377
|
await audioTransceiver.sender.replaceTrack(track);
|
|
330
378
|
// Update direction to sendrecv
|
|
331
379
|
audioTransceiver.direction = "sendrecv";
|
|
380
|
+
this.#debug("Replaced track in existing transceiver");
|
|
332
381
|
}
|
|
333
382
|
else {
|
|
334
383
|
// Add track normally
|
|
335
384
|
stream.getTracks().forEach((track) => {
|
|
336
385
|
this.#pc.addTrack(track, stream);
|
|
337
386
|
});
|
|
387
|
+
this.#debug("Added tracks to peer connection");
|
|
338
388
|
}
|
|
339
389
|
}
|
|
390
|
+
this.#debug("Microphone enabled successfully");
|
|
340
391
|
return true;
|
|
341
392
|
}
|
|
342
393
|
catch (e) {
|
|
343
|
-
|
|
344
|
-
this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
|
|
394
|
+
this.#logger.error("[WebRtcManager] Failed to get user media:", e);
|
|
395
|
+
this.#pubsub.publish(WebRtcManager.EVENT_MICROPHONE_FAILED, {
|
|
396
|
+
error: e,
|
|
397
|
+
});
|
|
345
398
|
return false;
|
|
346
399
|
}
|
|
347
400
|
}
|
|
348
401
|
else {
|
|
349
|
-
if (!this.#localStream)
|
|
402
|
+
if (!this.#localStream) {
|
|
403
|
+
this.#debug("Microphone already disabled");
|
|
350
404
|
return true;
|
|
405
|
+
}
|
|
406
|
+
this.#debug("Disabling microphone...");
|
|
351
407
|
this.#localStream.getTracks().forEach((track) => {
|
|
352
408
|
track.stop();
|
|
353
409
|
// Remove from PC if needed, or just stop sending
|
|
@@ -361,6 +417,7 @@ export class WebRtcManager {
|
|
|
361
417
|
});
|
|
362
418
|
this.#localStream = null;
|
|
363
419
|
this.#pubsub.publish(WebRtcManager.EVENT_LOCAL_STREAM, null);
|
|
420
|
+
this.#debug("Microphone disabled");
|
|
364
421
|
return true;
|
|
365
422
|
}
|
|
366
423
|
}
|
|
@@ -369,6 +426,7 @@ export class WebRtcManager {
|
|
|
369
426
|
* Transitions to DISCONNECTED state.
|
|
370
427
|
*/
|
|
371
428
|
disconnect() {
|
|
429
|
+
this.#debug("disconnect() called");
|
|
372
430
|
this.#cleanup();
|
|
373
431
|
this.#dispatch(WebRtcFsmEvent.DISCONNECT);
|
|
374
432
|
}
|
|
@@ -377,6 +435,7 @@ export class WebRtcManager {
|
|
|
377
435
|
* Cleans up all resources and allows reinitialization.
|
|
378
436
|
*/
|
|
379
437
|
reset() {
|
|
438
|
+
this.#debug("reset() called, current state:", this.state);
|
|
380
439
|
this.#cleanup();
|
|
381
440
|
// Reset from any non-IDLE state
|
|
382
441
|
if (this.state !== WebRtcState.IDLE) {
|
|
@@ -392,6 +451,7 @@ export class WebRtcManager {
|
|
|
392
451
|
this.#dispatch(WebRtcFsmEvent.RESET);
|
|
393
452
|
}
|
|
394
453
|
}
|
|
454
|
+
this.#debug("Reset complete, state:", this.state);
|
|
395
455
|
}
|
|
396
456
|
/**
|
|
397
457
|
* Creates a new data channel with the specified label.
|
|
@@ -401,14 +461,20 @@ export class WebRtcManager {
|
|
|
401
461
|
* @returns The created data channel, or null if peer connection not initialized.
|
|
402
462
|
*/
|
|
403
463
|
createDataChannel(label, options) {
|
|
404
|
-
|
|
464
|
+
this.#debug("createDataChannel() called:", label);
|
|
465
|
+
if (!this.#pc) {
|
|
466
|
+
this.#debug("Cannot create data channel: peer connection not initialized");
|
|
405
467
|
return null;
|
|
406
|
-
|
|
468
|
+
}
|
|
469
|
+
if (this.#dataChannels.has(label)) {
|
|
470
|
+
this.#debug("Returning existing data channel:", label);
|
|
407
471
|
return this.#dataChannels.get(label);
|
|
472
|
+
}
|
|
408
473
|
try {
|
|
409
474
|
const dc = this.#pc.createDataChannel(label, options);
|
|
410
475
|
this.#setupDataChannelListeners(dc);
|
|
411
476
|
this.#dataChannels.set(label, dc);
|
|
477
|
+
this.#debug("Data channel created:", label);
|
|
412
478
|
return dc;
|
|
413
479
|
}
|
|
414
480
|
catch (e) {
|
|
@@ -457,10 +523,14 @@ export class WebRtcManager {
|
|
|
457
523
|
* @returns The offer SDP, or null if peer connection not initialized.
|
|
458
524
|
*/
|
|
459
525
|
async createOffer(options) {
|
|
460
|
-
|
|
526
|
+
this.#debug("createOffer() called");
|
|
527
|
+
if (!this.#pc) {
|
|
528
|
+
this.#debug("Cannot create offer: peer connection not initialized");
|
|
461
529
|
return null;
|
|
530
|
+
}
|
|
462
531
|
try {
|
|
463
532
|
const offer = await this.#pc.createOffer(options);
|
|
533
|
+
this.#debug("Offer created:", offer.type);
|
|
464
534
|
return offer;
|
|
465
535
|
}
|
|
466
536
|
catch (e) {
|
|
@@ -474,10 +544,14 @@ export class WebRtcManager {
|
|
|
474
544
|
* @returns The answer SDP, or null if peer connection not initialized.
|
|
475
545
|
*/
|
|
476
546
|
async createAnswer(options) {
|
|
477
|
-
|
|
547
|
+
this.#debug("createAnswer() called");
|
|
548
|
+
if (!this.#pc) {
|
|
549
|
+
this.#debug("Cannot create answer: peer connection not initialized");
|
|
478
550
|
return null;
|
|
551
|
+
}
|
|
479
552
|
try {
|
|
480
553
|
const answer = await this.#pc.createAnswer(options);
|
|
554
|
+
this.#debug("Answer created:", answer.type);
|
|
481
555
|
return answer;
|
|
482
556
|
}
|
|
483
557
|
catch (e) {
|
|
@@ -491,10 +565,14 @@ export class WebRtcManager {
|
|
|
491
565
|
* @returns True if successful, false otherwise.
|
|
492
566
|
*/
|
|
493
567
|
async setLocalDescription(description) {
|
|
494
|
-
|
|
568
|
+
this.#debug("setLocalDescription() called:", description.type);
|
|
569
|
+
if (!this.#pc) {
|
|
570
|
+
this.#debug("Cannot set local description: peer connection not initialized");
|
|
495
571
|
return false;
|
|
572
|
+
}
|
|
496
573
|
try {
|
|
497
574
|
await this.#pc.setLocalDescription(description);
|
|
575
|
+
this.#debug("Local description set successfully");
|
|
498
576
|
return true;
|
|
499
577
|
}
|
|
500
578
|
catch (e) {
|
|
@@ -508,10 +586,14 @@ export class WebRtcManager {
|
|
|
508
586
|
* @returns True if successful, false otherwise.
|
|
509
587
|
*/
|
|
510
588
|
async setRemoteDescription(description) {
|
|
511
|
-
|
|
589
|
+
this.#debug("setRemoteDescription() called:", description.type);
|
|
590
|
+
if (!this.#pc) {
|
|
591
|
+
this.#debug("Cannot set remote description: peer connection not initialized");
|
|
512
592
|
return false;
|
|
593
|
+
}
|
|
513
594
|
try {
|
|
514
595
|
await this.#pc.setRemoteDescription(description);
|
|
596
|
+
this.#debug("Remote description set successfully");
|
|
515
597
|
return true;
|
|
516
598
|
}
|
|
517
599
|
catch (e) {
|
|
@@ -525,11 +607,15 @@ export class WebRtcManager {
|
|
|
525
607
|
* @returns True if successful, false otherwise.
|
|
526
608
|
*/
|
|
527
609
|
async addIceCandidate(candidate) {
|
|
528
|
-
|
|
610
|
+
this.#debug("addIceCandidate() called:", candidate ? "candidate" : "null (end-of-candidates)");
|
|
611
|
+
if (!this.#pc) {
|
|
612
|
+
this.#debug("Cannot add ICE candidate: peer connection not initialized");
|
|
529
613
|
return false;
|
|
614
|
+
}
|
|
530
615
|
try {
|
|
531
616
|
if (candidate) {
|
|
532
617
|
await this.#pc.addIceCandidate(candidate);
|
|
618
|
+
this.#debug("ICE candidate added");
|
|
533
619
|
}
|
|
534
620
|
return true;
|
|
535
621
|
}
|
|
@@ -544,11 +630,15 @@ export class WebRtcManager {
|
|
|
544
630
|
* @returns True if successful, false otherwise.
|
|
545
631
|
*/
|
|
546
632
|
async iceRestart() {
|
|
547
|
-
|
|
633
|
+
this.#debug("iceRestart() called");
|
|
634
|
+
if (!this.#pc) {
|
|
635
|
+
this.#debug("Cannot perform ICE restart: peer connection not initialized");
|
|
548
636
|
return false;
|
|
637
|
+
}
|
|
549
638
|
try {
|
|
550
639
|
const offer = await this.#pc.createOffer({ iceRestart: true });
|
|
551
640
|
await this.#pc.setLocalDescription(offer);
|
|
641
|
+
this.#debug("ICE restart initiated");
|
|
552
642
|
return true;
|
|
553
643
|
}
|
|
554
644
|
catch (e) {
|
|
@@ -591,24 +681,29 @@ export class WebRtcManager {
|
|
|
591
681
|
this.#fsm.transition(event);
|
|
592
682
|
const newState = this.#fsm.state;
|
|
593
683
|
if (oldState !== newState) {
|
|
684
|
+
this.#debug("State transition:", oldState, "->", newState, "(event:", event + ")");
|
|
594
685
|
this.#pubsub.publish(WebRtcManager.EVENT_STATE_CHANGE, newState);
|
|
595
686
|
}
|
|
596
687
|
}
|
|
688
|
+
// deno-lint-ignore no-explicit-any
|
|
597
689
|
#debug(...args) {
|
|
598
690
|
if (this.#config.debug) {
|
|
599
|
-
|
|
691
|
+
this.#logger.debug("[WebRtcManager]", ...args);
|
|
600
692
|
}
|
|
601
693
|
}
|
|
694
|
+
// deno-lint-ignore no-explicit-any
|
|
602
695
|
#error(error) {
|
|
603
|
-
|
|
696
|
+
this.#logger.error("[WebRtcManager]", error);
|
|
604
697
|
this.#dispatch(WebRtcFsmEvent.ERROR);
|
|
605
698
|
this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
|
|
606
699
|
}
|
|
607
700
|
#setupPcListeners() {
|
|
608
701
|
if (!this.#pc)
|
|
609
702
|
return;
|
|
703
|
+
this.#debug("Setting up peer connection listeners");
|
|
610
704
|
this.#pc.onconnectionstatechange = () => {
|
|
611
705
|
const state = this.#pc.connectionState;
|
|
706
|
+
this.#debug("Connection state changed:", state);
|
|
612
707
|
if (state === "connected") {
|
|
613
708
|
// Connection successful - reset reconnect attempts
|
|
614
709
|
this.#reconnectAttempts = 0;
|
|
@@ -619,10 +714,16 @@ export class WebRtcManager {
|
|
|
619
714
|
this.#handleConnectionFailure();
|
|
620
715
|
}
|
|
621
716
|
else if (state === "disconnected" || state === "closed") {
|
|
622
|
-
|
|
717
|
+
// Only dispatch if not already in a terminal state
|
|
718
|
+
if (this.state !== WebRtcState.DISCONNECTED &&
|
|
719
|
+
this.state !== WebRtcState.ERROR &&
|
|
720
|
+
this.state !== WebRtcState.IDLE) {
|
|
721
|
+
this.#dispatch(WebRtcFsmEvent.DISCONNECT);
|
|
722
|
+
}
|
|
623
723
|
}
|
|
624
724
|
};
|
|
625
725
|
this.#pc.ontrack = (event) => {
|
|
726
|
+
this.#debug("Remote track received:", event.track.kind);
|
|
626
727
|
if (event.streams && event.streams[0]) {
|
|
627
728
|
this.#remoteStream = event.streams[0];
|
|
628
729
|
this.#pubsub.publish(WebRtcManager.EVENT_REMOTE_STREAM, this.#remoteStream);
|
|
@@ -630,14 +731,17 @@ export class WebRtcManager {
|
|
|
630
731
|
};
|
|
631
732
|
this.#pc.ondatachannel = (event) => {
|
|
632
733
|
const dc = event.channel;
|
|
734
|
+
this.#debug("Remote data channel received:", dc.label);
|
|
633
735
|
this.#setupDataChannelListeners(dc);
|
|
634
736
|
this.#dataChannels.set(dc.label, dc);
|
|
635
737
|
};
|
|
636
738
|
this.#pc.onicecandidate = (event) => {
|
|
739
|
+
this.#debug("ICE candidate generated:", event.candidate ? "candidate" : "null (gathering complete)");
|
|
637
740
|
this.#pubsub.publish(WebRtcManager.EVENT_ICE_CANDIDATE, event.candidate);
|
|
638
741
|
};
|
|
639
742
|
}
|
|
640
743
|
#cleanup() {
|
|
744
|
+
this.#debug("Cleanup started");
|
|
641
745
|
// Clear any pending reconnect timers
|
|
642
746
|
if (this.#reconnectTimer !== null) {
|
|
643
747
|
clearTimeout(this.#reconnectTimer);
|
|
@@ -649,33 +753,48 @@ export class WebRtcManager {
|
|
|
649
753
|
this.#deviceChangeHandler = null;
|
|
650
754
|
}
|
|
651
755
|
// Close all data channels
|
|
756
|
+
const dcCount = this.#dataChannels.size;
|
|
652
757
|
this.#dataChannels.forEach((dc) => {
|
|
653
758
|
if (dc.readyState !== "closed") {
|
|
654
759
|
dc.close();
|
|
655
760
|
}
|
|
656
761
|
});
|
|
657
762
|
this.#dataChannels.clear();
|
|
763
|
+
if (dcCount > 0) {
|
|
764
|
+
this.#debug("Closed", dcCount, "data channel(s)");
|
|
765
|
+
}
|
|
658
766
|
// Stop local stream tracks
|
|
659
767
|
if (this.#localStream) {
|
|
660
768
|
this.#localStream.getTracks().forEach((track) => track.stop());
|
|
661
769
|
this.#localStream = null;
|
|
770
|
+
this.#debug("Local stream stopped");
|
|
662
771
|
}
|
|
663
772
|
// Close peer connection
|
|
664
773
|
if (this.#pc) {
|
|
665
774
|
this.#pc.close();
|
|
666
775
|
this.#pc = null;
|
|
776
|
+
this.#debug("Peer connection closed");
|
|
667
777
|
}
|
|
668
778
|
this.#remoteStream = null;
|
|
779
|
+
this.#debug("Cleanup complete");
|
|
669
780
|
}
|
|
670
781
|
#handleConnectionFailure() {
|
|
671
|
-
this.#
|
|
782
|
+
this.#debug("Handling connection failure");
|
|
783
|
+
// Only dispatch DISCONNECT if not already in a terminal state
|
|
784
|
+
if (this.state !== WebRtcState.DISCONNECTED &&
|
|
785
|
+
this.state !== WebRtcState.ERROR &&
|
|
786
|
+
this.state !== WebRtcState.IDLE) {
|
|
787
|
+
this.#dispatch(WebRtcFsmEvent.DISCONNECT);
|
|
788
|
+
}
|
|
672
789
|
// Check if auto-reconnect is enabled
|
|
673
790
|
if (!this.#config.autoReconnect) {
|
|
791
|
+
this.#debug("Auto-reconnect disabled, not attempting reconnection");
|
|
674
792
|
return;
|
|
675
793
|
}
|
|
676
794
|
const maxAttempts = this.#config.maxReconnectAttempts ?? 5;
|
|
677
795
|
// Check if we've exceeded max attempts
|
|
678
796
|
if (this.#reconnectAttempts >= maxAttempts) {
|
|
797
|
+
this.#debug("Max reconnection attempts reached:", maxAttempts);
|
|
679
798
|
this.#pubsub.publish(WebRtcManager.EVENT_RECONNECT_FAILED, {
|
|
680
799
|
attempts: this.#reconnectAttempts,
|
|
681
800
|
});
|
|
@@ -692,6 +811,11 @@ export class WebRtcManager {
|
|
|
692
811
|
const delay = baseDelay * Math.pow(2, this.#reconnectAttempts - 1);
|
|
693
812
|
// Try ICE restart first (attempts 1-2), then full reconnect
|
|
694
813
|
const strategy = this.#reconnectAttempts <= 2 ? "ice-restart" : "full";
|
|
814
|
+
this.#debug("Attempting reconnection:", {
|
|
815
|
+
attempt: this.#reconnectAttempts,
|
|
816
|
+
strategy,
|
|
817
|
+
delay: delay + "ms",
|
|
818
|
+
});
|
|
695
819
|
this.#pubsub.publish(WebRtcManager.EVENT_RECONNECTING, {
|
|
696
820
|
attempt: this.#reconnectAttempts,
|
|
697
821
|
strategy,
|
|
@@ -718,7 +842,7 @@ export class WebRtcManager {
|
|
|
718
842
|
// If successful, onconnectionstatechange will reset attempts
|
|
719
843
|
}
|
|
720
844
|
catch (e) {
|
|
721
|
-
|
|
845
|
+
this.#logger.error("[WebRtcManager] Reconnection failed:", e);
|
|
722
846
|
this.#handleConnectionFailure();
|
|
723
847
|
}
|
|
724
848
|
}
|
|
@@ -739,7 +863,7 @@ export class WebRtcManager {
|
|
|
739
863
|
this.#pubsub.publish(WebRtcManager.EVENT_DEVICE_CHANGED, devices);
|
|
740
864
|
}
|
|
741
865
|
catch (e) {
|
|
742
|
-
|
|
866
|
+
this.#logger.error("[WebRtcManager] Error handling device change:", e);
|
|
743
867
|
}
|
|
744
868
|
};
|
|
745
869
|
navigator.mediaDevices.addEventListener("devicechange", this.#deviceChangeHandler);
|
|
@@ -758,11 +882,12 @@ export class WebRtcManager {
|
|
|
758
882
|
this.#pubsub.publish(WebRtcManager.EVENT_DATA_CHANNEL_CLOSE, dc);
|
|
759
883
|
this.#dataChannels.delete(dc.label);
|
|
760
884
|
};
|
|
885
|
+
// deno-lint-ignore no-explicit-any
|
|
761
886
|
dc.onerror = (error) => {
|
|
762
887
|
// Ignore "User-Initiated Abort" errors which occur during intentional close()
|
|
763
888
|
const isUserAbort = error?.error?.message?.includes("User-Initiated Abort");
|
|
764
889
|
if (!isUserAbort) {
|
|
765
|
-
|
|
890
|
+
this.#logger.error("[WebRtcManager] Data channel error:", error);
|
|
766
891
|
this.#pubsub.publish(WebRtcManager.EVENT_ERROR, error);
|
|
767
892
|
}
|
|
768
893
|
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/webrtc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/mod.js",
|
|
6
6
|
"types": "dist/mod.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/mod.d.ts",
|
|
10
|
+
"import": "./dist/mod.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
7
13
|
"author": "Marian Meres",
|
|
8
14
|
"license": "MIT",
|
|
9
15
|
"dependencies": {
|
|
10
|
-
"@marianmeres/fsm": "^2.
|
|
11
|
-
"@marianmeres/pubsub": "^2.4.
|
|
16
|
+
"@marianmeres/fsm": "^2.11.0",
|
|
17
|
+
"@marianmeres/pubsub": "^2.4.4"
|
|
12
18
|
},
|
|
13
19
|
"repository": {
|
|
14
20
|
"type": "git",
|
package/llm.txt
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
# @marianmeres/webrtc - LLM Knowledge Base
|
|
2
|
-
|
|
3
|
-
## Package Identity
|
|
4
|
-
|
|
5
|
-
name: @marianmeres/webrtc
|
|
6
|
-
version: 0.0.2
|
|
7
|
-
license: MIT
|
|
8
|
-
author: Marian Meres
|
|
9
|
-
repository: https://github.com/marianmeres/webrtc
|
|
10
|
-
runtime: Deno (source), Node.js/Browser (distribution)
|
|
11
|
-
type: WebRTC connection management library
|
|
12
|
-
|
|
13
|
-
## Purpose
|
|
14
|
-
|
|
15
|
-
A lightweight, framework-agnostic WebRTC manager providing:
|
|
16
|
-
- Finite State Machine (FSM) based lifecycle management
|
|
17
|
-
- Event-driven architecture with PubSub pattern
|
|
18
|
-
- Svelte store compatibility
|
|
19
|
-
- Audio device management (microphone switching)
|
|
20
|
-
- Data channel support
|
|
21
|
-
- Auto-reconnection with exponential backoff
|
|
22
|
-
- Full TypeScript type safety
|
|
23
|
-
|
|
24
|
-
## Architecture Overview
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
WebRtcManager
|
|
28
|
-
├── FSM (@marianmeres/fsm) - State transitions
|
|
29
|
-
├── PubSub (@marianmeres/pubsub) - Event subscriptions
|
|
30
|
-
├── RTCPeerConnection - WebRTC connection (via factory)
|
|
31
|
-
├── MediaStream (local/remote) - Audio streams
|
|
32
|
-
└── DataChannels Map - RTCDataChannel instances
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Dependencies
|
|
36
|
-
|
|
37
|
-
Production:
|
|
38
|
-
- @marianmeres/fsm: ^2.3.0 (state machine)
|
|
39
|
-
- @marianmeres/pubsub: ^2.3.0 (event system)
|
|
40
|
-
|
|
41
|
-
Development (Deno):
|
|
42
|
-
- @std/assert: testing
|
|
43
|
-
- @std/fs: file operations
|
|
44
|
-
- @std/path: path utilities
|
|
45
|
-
|
|
46
|
-
## File Structure
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
src/
|
|
50
|
-
├── mod.ts # Entry point, re-exports all public APIs
|
|
51
|
-
├── types.ts # Type definitions (interfaces, enums)
|
|
52
|
-
└── webrtc-manager.ts # Main WebRtcManager class (839 lines)
|
|
53
|
-
|
|
54
|
-
tests/
|
|
55
|
-
├── mocks.ts # Mock WebRtcFactory for testing
|
|
56
|
-
├── webrtc-manager.test.ts # Deno unit tests
|
|
57
|
-
└── browser/
|
|
58
|
-
├── p2p-tests.ts # Browser integration tests
|
|
59
|
-
└── README.md # Browser test documentation
|
|
60
|
-
|
|
61
|
-
example/
|
|
62
|
-
├── peer.ts # Two-peer example with localStorage signaling
|
|
63
|
-
├── p2p.ts # Single-page P2P example
|
|
64
|
-
├── audio-peer.ts # Audio testing implementation
|
|
65
|
-
└── main.ts # Signaling server example
|
|
66
|
-
|
|
67
|
-
scripts/
|
|
68
|
-
├── build-npm.ts # npm distribution build
|
|
69
|
-
├── build-example.ts # Example bundling
|
|
70
|
-
├── build-browser-tests.ts
|
|
71
|
-
├── serve-browser-tests.ts
|
|
72
|
-
└── signaling-server.ts
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## State Machine
|
|
76
|
-
|
|
77
|
-
### States (WebRtcState enum)
|
|
78
|
-
|
|
79
|
-
| State | Description | Valid Transitions |
|
|
80
|
-
|-------|-------------|-------------------|
|
|
81
|
-
| IDLE | Initial state, no resources | → INITIALIZING |
|
|
82
|
-
| INITIALIZING | Creating peer connection | → CONNECTING, ERROR |
|
|
83
|
-
| CONNECTING | Performing SDP exchange | → CONNECTED, DISCONNECTED, ERROR |
|
|
84
|
-
| CONNECTED | Connection established | → DISCONNECTED, ERROR |
|
|
85
|
-
| RECONNECTING | Auto-reconnection in progress | → CONNECTING, DISCONNECTED, IDLE |
|
|
86
|
-
| DISCONNECTED | Closed but recoverable | → CONNECTING, RECONNECTING, IDLE |
|
|
87
|
-
| ERROR | Error occurred, must reset | → IDLE |
|
|
88
|
-
|
|
89
|
-
### Events (WebRtcFsmEvent enum)
|
|
90
|
-
|
|
91
|
-
| Event | Description |
|
|
92
|
-
|-------|-------------|
|
|
93
|
-
| INIT ("initialize") | Start initialization |
|
|
94
|
-
| CONNECT ("connect") | Begin connection |
|
|
95
|
-
| CONNECTED ("connected") | Connection succeeded |
|
|
96
|
-
| RECONNECTING ("reconnecting") | Start reconnection |
|
|
97
|
-
| DISCONNECT ("disconnect") | Close connection |
|
|
98
|
-
| ERROR ("error") | Error occurred |
|
|
99
|
-
| RESET ("reset") | Return to IDLE |
|
|
100
|
-
|
|
101
|
-
### State Transition Diagram
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
IDLE --INIT--> INITIALIZING --CONNECT--> CONNECTING --CONNECTED--> CONNECTED
|
|
105
|
-
| | |
|
|
106
|
-
v v v
|
|
107
|
-
ERROR <-----------------ERROR<----------------------ERROR
|
|
108
|
-
|
|
|
109
|
-
v
|
|
110
|
-
IDLE (via RESET)
|
|
111
|
-
|
|
112
|
-
CONNECTED --DISCONNECT--> DISCONNECTED --RESET--> IDLE
|
|
113
|
-
|
|
|
114
|
-
v
|
|
115
|
-
RECONNECTING --CONNECT--> CONNECTING
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## Public API
|
|
119
|
-
|
|
120
|
-
### Constructor
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
new WebRtcManager(factory: WebRtcFactory, config?: WebRtcManagerConfig)
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### WebRtcFactory Interface
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
interface WebRtcFactory {
|
|
130
|
-
createPeerConnection(config?: RTCConfiguration): RTCPeerConnection;
|
|
131
|
-
getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
|
132
|
-
enumerateDevices(): Promise<MediaDeviceInfo[]>;
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Browser implementation:
|
|
137
|
-
```typescript
|
|
138
|
-
const factory = {
|
|
139
|
-
createPeerConnection: (config) => new RTCPeerConnection(config),
|
|
140
|
-
getUserMedia: (constraints) => navigator.mediaDevices.getUserMedia(constraints),
|
|
141
|
-
enumerateDevices: () => navigator.mediaDevices.enumerateDevices(),
|
|
142
|
-
};
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### WebRtcManagerConfig Interface
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
interface WebRtcManagerConfig {
|
|
149
|
-
peerConfig?: RTCConfiguration; // ICE servers, certificates
|
|
150
|
-
enableMicrophone?: boolean; // Enable mic on init (default: false)
|
|
151
|
-
dataChannelLabel?: string; // Auto-create data channel
|
|
152
|
-
autoReconnect?: boolean; // Enable auto-reconnect (default: false)
|
|
153
|
-
maxReconnectAttempts?: number; // Max attempts (default: 5)
|
|
154
|
-
reconnectDelay?: number; // Initial delay ms (default: 1000)
|
|
155
|
-
debug?: boolean; // Enable logging (default: false)
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Properties (Getters)
|
|
160
|
-
|
|
161
|
-
| Property | Type | Description |
|
|
162
|
-
|----------|------|-------------|
|
|
163
|
-
| state | WebRtcState | Current FSM state |
|
|
164
|
-
| localStream | MediaStream \| null | Local audio stream |
|
|
165
|
-
| remoteStream | MediaStream \| null | Remote audio stream |
|
|
166
|
-
| dataChannels | ReadonlyMap<string, RTCDataChannel> | Active data channels |
|
|
167
|
-
| peerConnection | RTCPeerConnection \| null | Underlying connection |
|
|
168
|
-
|
|
169
|
-
### Lifecycle Methods
|
|
170
|
-
|
|
171
|
-
| Method | Signature | Description |
|
|
172
|
-
|--------|-----------|-------------|
|
|
173
|
-
| initialize | `(): Promise<void>` | Create peer connection, setup tracks |
|
|
174
|
-
| connect | `(): Promise<void>` | Transition to CONNECTING (auto-initializes) |
|
|
175
|
-
| disconnect | `(): void` | Close connection, cleanup resources |
|
|
176
|
-
| reset | `(): void` | Reset to IDLE from any state |
|
|
177
|
-
|
|
178
|
-
### Audio Methods
|
|
179
|
-
|
|
180
|
-
| Method | Signature | Returns | Description |
|
|
181
|
-
|--------|-----------|---------|-------------|
|
|
182
|
-
| enableMicrophone | `(enable: boolean): Promise<boolean>` | success | Enable/disable microphone |
|
|
183
|
-
| switchMicrophone | `(deviceId: string): Promise<boolean>` | success | Switch audio input device |
|
|
184
|
-
| getAudioInputDevices | `(): Promise<MediaDeviceInfo[]>` | devices | List audio inputs |
|
|
185
|
-
|
|
186
|
-
### Data Channel Methods
|
|
187
|
-
|
|
188
|
-
| Method | Signature | Returns | Description |
|
|
189
|
-
|--------|-----------|---------|-------------|
|
|
190
|
-
| createDataChannel | `(label: string, options?: RTCDataChannelInit): RTCDataChannel \| null` | channel | Create/get data channel |
|
|
191
|
-
| getDataChannel | `(label: string): RTCDataChannel \| undefined` | channel | Get existing channel |
|
|
192
|
-
| sendData | `(label: string, data: string \| Blob \| ArrayBuffer \| ArrayBufferView): boolean` | success | Send through channel |
|
|
193
|
-
|
|
194
|
-
### Signaling Methods
|
|
195
|
-
|
|
196
|
-
| Method | Signature | Returns | Description |
|
|
197
|
-
|--------|-----------|---------|-------------|
|
|
198
|
-
| createOffer | `(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit \| null>` | offer | Create SDP offer |
|
|
199
|
-
| createAnswer | `(options?: RTCAnswerOptions): Promise<RTCSessionDescriptionInit \| null>` | answer | Create SDP answer |
|
|
200
|
-
| setLocalDescription | `(description: RTCSessionDescriptionInit): Promise<boolean>` | success | Set local SDP |
|
|
201
|
-
| setRemoteDescription | `(description: RTCSessionDescriptionInit): Promise<boolean>` | success | Set remote SDP |
|
|
202
|
-
| addIceCandidate | `(candidate: RTCIceCandidateInit \| null): Promise<boolean>` | success | Add ICE candidate |
|
|
203
|
-
| iceRestart | `(): Promise<boolean>` | success | Perform ICE restart |
|
|
204
|
-
| getLocalDescription | `(): RTCSessionDescription \| null` | description | Get local SDP |
|
|
205
|
-
| getRemoteDescription | `(): RTCSessionDescription \| null` | description | Get remote SDP |
|
|
206
|
-
| getStats | `(): Promise<RTCStatsReport \| null>` | stats | Get connection statistics |
|
|
207
|
-
|
|
208
|
-
### Event Subscription Methods
|
|
209
|
-
|
|
210
|
-
| Method | Signature | Returns | Description |
|
|
211
|
-
|--------|-----------|---------|-------------|
|
|
212
|
-
| on | `<K extends keyof WebRtcEvents>(event: K, handler: (data: WebRtcEvents[K]) => void): () => void` | unsubscribe | Subscribe to specific event |
|
|
213
|
-
| subscribe | `(handler: (state: OverallState) => void): () => void` | unsubscribe | Subscribe to overall state (Svelte compatible) |
|
|
214
|
-
|
|
215
|
-
### Utility Methods
|
|
216
|
-
|
|
217
|
-
| Method | Signature | Returns | Description |
|
|
218
|
-
|--------|-----------|---------|-------------|
|
|
219
|
-
| toMermaid | `(): string` | mermaid | Get FSM as Mermaid diagram |
|
|
220
|
-
|
|
221
|
-
## Events
|
|
222
|
-
|
|
223
|
-
### Event Constants (Static)
|
|
224
|
-
|
|
225
|
-
| Constant | Value | Payload Type |
|
|
226
|
-
|----------|-------|--------------|
|
|
227
|
-
| EVENT_STATE_CHANGE | "state_change" | WebRtcState |
|
|
228
|
-
| EVENT_LOCAL_STREAM | "local_stream" | MediaStream \| null |
|
|
229
|
-
| EVENT_REMOTE_STREAM | "remote_stream" | MediaStream \| null |
|
|
230
|
-
| EVENT_DATA_CHANNEL_OPEN | "data_channel_open" | RTCDataChannel |
|
|
231
|
-
| EVENT_DATA_CHANNEL_MESSAGE | "data_channel_message" | { channel: RTCDataChannel; data: any } |
|
|
232
|
-
| EVENT_DATA_CHANNEL_CLOSE | "data_channel_close" | RTCDataChannel |
|
|
233
|
-
| EVENT_ICE_CANDIDATE | "ice_candidate" | RTCIceCandidate \| null |
|
|
234
|
-
| EVENT_RECONNECTING | "reconnecting" | { attempt: number; strategy: "ice-restart" \| "full" } |
|
|
235
|
-
| EVENT_RECONNECT_FAILED | "reconnect_failed" | { attempts: number } |
|
|
236
|
-
| EVENT_DEVICE_CHANGED | "device_changed" | MediaDeviceInfo[] |
|
|
237
|
-
| EVENT_MICROPHONE_FAILED | "microphone_failed" | { error?: any; reason?: string } |
|
|
238
|
-
| EVENT_ERROR | "error" | Error |
|
|
239
|
-
|
|
240
|
-
### WebRtcEvents Interface
|
|
241
|
-
|
|
242
|
-
```typescript
|
|
243
|
-
interface WebRtcEvents {
|
|
244
|
-
state_change: WebRtcState;
|
|
245
|
-
local_stream: MediaStream | null;
|
|
246
|
-
remote_stream: MediaStream | null;
|
|
247
|
-
data_channel_open: RTCDataChannel;
|
|
248
|
-
data_channel_message: { channel: RTCDataChannel; data: any };
|
|
249
|
-
data_channel_close: RTCDataChannel;
|
|
250
|
-
ice_candidate: RTCIceCandidate | null;
|
|
251
|
-
reconnecting: { attempt: number; strategy: "ice-restart" | "full" };
|
|
252
|
-
reconnect_failed: { attempts: number };
|
|
253
|
-
device_changed: MediaDeviceInfo[];
|
|
254
|
-
microphone_failed: { error?: any; reason?: string };
|
|
255
|
-
error: Error;
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
## Signaling Flow (User Responsibility)
|
|
260
|
-
|
|
261
|
-
The library does NOT handle signaling transport. Users must:
|
|
262
|
-
|
|
263
|
-
1. Create signaling channel (WebSocket, HTTP, localStorage, etc.)
|
|
264
|
-
2. Listen for `ice_candidate` events and send to remote peer
|
|
265
|
-
3. Send offers/answers through signaling channel
|
|
266
|
-
4. Receive remote offers/answers and call setRemoteDescription
|
|
267
|
-
5. Receive remote ICE candidates and call addIceCandidate
|
|
268
|
-
|
|
269
|
-
### Offer/Answer Flow
|
|
270
|
-
|
|
271
|
-
```
|
|
272
|
-
Initiator: Responder:
|
|
273
|
-
1. initialize()
|
|
274
|
-
2. connect()
|
|
275
|
-
3. createOffer()
|
|
276
|
-
4. setLocalDescription(offer)
|
|
277
|
-
5. [send offer via signaling] ───→ 6. initialize()
|
|
278
|
-
7. setRemoteDescription(offer)
|
|
279
|
-
8. createAnswer()
|
|
280
|
-
9. setLocalDescription(answer)
|
|
281
|
-
10. setRemoteDescription(answer) ←── [send answer via signaling]
|
|
282
|
-
11. [exchange ICE candidates] ←───→ [exchange ICE candidates]
|
|
283
|
-
12. CONNECTED 12. CONNECTED
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## Reconnection Strategy
|
|
287
|
-
|
|
288
|
-
When autoReconnect is enabled:
|
|
289
|
-
|
|
290
|
-
1. Attempts 1-2: ICE restart (preserves connection, quick recovery)
|
|
291
|
-
2. Attempts 3+: Full reconnection (new peer connection)
|
|
292
|
-
3. Exponential backoff: delay * 2^(attempt-1)
|
|
293
|
-
4. Max attempts configurable (default: 5)
|
|
294
|
-
|
|
295
|
-
For "full" strategy reconnections, consumers MUST:
|
|
296
|
-
- Listen for `reconnecting` event with strategy="full"
|
|
297
|
-
- Re-perform signaling handshake (create new offer/answer)
|
|
298
|
-
|
|
299
|
-
## Error Handling Patterns
|
|
300
|
-
|
|
301
|
-
1. Methods return boolean for success/failure
|
|
302
|
-
2. Critical errors transition to ERROR state
|
|
303
|
-
3. ERROR state requires reset() to recover
|
|
304
|
-
4. EVENT_ERROR emitted for all errors
|
|
305
|
-
5. Specific events: EVENT_MICROPHONE_FAILED, EVENT_RECONNECT_FAILED
|
|
306
|
-
|
|
307
|
-
## Testing
|
|
308
|
-
|
|
309
|
-
### Unit Tests (Deno)
|
|
310
|
-
```bash
|
|
311
|
-
deno task test
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Browser Integration Tests
|
|
315
|
-
```bash
|
|
316
|
-
deno task test:browser
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
## Build Commands
|
|
320
|
-
|
|
321
|
-
```bash
|
|
322
|
-
deno task npm:build # Build npm distribution
|
|
323
|
-
deno task npm:publish # Build and publish to npm
|
|
324
|
-
deno task build:example # Build examples
|
|
325
|
-
deno task serve:example # Run signaling server
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
## Important Implementation Details
|
|
329
|
-
|
|
330
|
-
1. subscribe() is Svelte store compatible (immediate callback + updates)
|
|
331
|
-
2. Data channels auto-cleanup on close
|
|
332
|
-
3. Device change listener auto-setup on initialize
|
|
333
|
-
4. "User-Initiated Abort" errors from intentional close() are ignored
|
|
334
|
-
5. recvonly transceiver added when microphone disabled (ensures audio SDP)
|
|
335
|
-
6. Private fields use # (true private, not accessible externally)
|
|
336
|
-
|
|
337
|
-
## Common Usage Patterns
|
|
338
|
-
|
|
339
|
-
### Minimal P2P Setup
|
|
340
|
-
```typescript
|
|
341
|
-
const manager = new WebRtcManager(factory, { enableMicrophone: true });
|
|
342
|
-
await manager.initialize();
|
|
343
|
-
await manager.connect();
|
|
344
|
-
const offer = await manager.createOffer();
|
|
345
|
-
await manager.setLocalDescription(offer);
|
|
346
|
-
// Send offer, receive answer, exchange ICE candidates...
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
### With Data Channel
|
|
350
|
-
```typescript
|
|
351
|
-
const manager = new WebRtcManager(factory, { dataChannelLabel: "chat" });
|
|
352
|
-
manager.on("data_channel_message", ({ data }) => console.log(data));
|
|
353
|
-
// After connection...
|
|
354
|
-
manager.sendData("chat", "Hello!");
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
### Svelte Integration
|
|
358
|
-
```svelte
|
|
359
|
-
<script>
|
|
360
|
-
const manager = new WebRtcManager(factory, config);
|
|
361
|
-
// $manager reactive access to state
|
|
362
|
-
</script>
|
|
363
|
-
{$manager.state}
|
|
364
|
-
```
|