@marianmeres/webrtc 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +408 -0
- package/dist/mod.d.ts +2 -0
- package/dist/mod.js +2 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +20 -0
- package/dist/webrtc-manager.d.ts +158 -0
- package/dist/webrtc-manager.js +726 -0
- package/package.json +20 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Marian Meres
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# @marianmeres/webrtc
|
|
2
|
+
|
|
3
|
+
> **Full Disclosure:** This code was written by Claude (Anthropic's AI). The human (that is @marianmeres) just asked nicely, occasionally said "thanks", and went through about 47 iterations of "could you improve this", "what about that", and "make it more Svelte-friendly". To be fair, the prompt engineering was top-notch. So if you find bugs, we'll split the blame 50/50. If it works perfectly, Claude gets 95% of the credit and @marianmeres gets the remaining 5% for excellent taste in asking the right questions. 🤖
|
|
4
|
+
|
|
5
|
+
A lightweight, framework-agnostic WebRTC manager with state machine-based lifecycle management and event-driven architecture.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **State Machine-based**: Clean state transitions (IDLE → INITIALIZING → CONNECTING → CONNECTED)
|
|
10
|
+
- **Event-driven**: Subscribe to specific events or overall state changes
|
|
11
|
+
- **Svelte Store Compatible**: Works seamlessly with Svelte's reactive `$` syntax
|
|
12
|
+
- **Audio Management**: Microphone enable/disable, device switching, device change detection
|
|
13
|
+
- **Data Channels**: Easy creation and management of RTCDataChannels
|
|
14
|
+
- **Auto-reconnection**: Optional automatic reconnection with exponential backoff
|
|
15
|
+
- **TypeScript**: Full type safety and excellent IDE support
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @marianmeres/webrtc
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## High-Level Overview
|
|
24
|
+
|
|
25
|
+
The `WebRtcManager` class handles the complete WebRTC connection lifecycle:
|
|
26
|
+
|
|
27
|
+
1. **Initialization**: Sets up RTCPeerConnection, media streams, and data channels
|
|
28
|
+
2. **Connection Management**: Handles state transitions, reconnection, and cleanup
|
|
29
|
+
3. **Signaling**: Provides methods for offer/answer exchange and ICE candidate handling
|
|
30
|
+
4. **Media Control**: Manages local/remote streams and microphone switching
|
|
31
|
+
5. **Events**: Emits events for all important state changes
|
|
32
|
+
|
|
33
|
+
The manager doesn't handle the signaling transport layer - you're responsible for sending/receiving offers, answers, and ICE candidates through your own signaling mechanism (WebSocket, HTTP, etc.).
|
|
34
|
+
|
|
35
|
+
## Core API
|
|
36
|
+
|
|
37
|
+
### Constructor
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const manager = new WebRtcManager(factory, config);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `factory`: Object implementing `WebRtcFactory` interface (provides `createPeerConnection`, `getUserMedia`, `enumerateDevices`)
|
|
44
|
+
- `config`: Optional configuration object
|
|
45
|
+
|
|
46
|
+
**Configuration Options:**
|
|
47
|
+
- `peerConfig`: RTCConfiguration (ICE servers, etc.)
|
|
48
|
+
- `enableMicrophone`: Enable microphone on initialization (default: false)
|
|
49
|
+
- `dataChannelLabel`: Create a default data channel with this label
|
|
50
|
+
- `autoReconnect`: Enable automatic reconnection (default: false)
|
|
51
|
+
- `maxReconnectAttempts`: Max reconnection attempts (default: 5)
|
|
52
|
+
- `reconnectDelay`: Initial reconnection delay in ms (default: 1000)
|
|
53
|
+
- `debug`: Enable debug logging (default: false)
|
|
54
|
+
|
|
55
|
+
### State and Properties
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
manager.state // Current WebRtcState
|
|
59
|
+
manager.localStream // MediaStream | null
|
|
60
|
+
manager.remoteStream // MediaStream | null
|
|
61
|
+
manager.dataChannels // ReadonlyMap<string, RTCDataChannel>
|
|
62
|
+
manager.peerConnection // RTCPeerConnection | null
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Lifecycle Methods
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
await manager.initialize() // Initialize peer connection
|
|
69
|
+
await manager.connect() // Transition to CONNECTING state
|
|
70
|
+
manager.disconnect() // Disconnect and cleanup
|
|
71
|
+
manager.reset() // Reset to IDLE state
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Audio Methods
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
await manager.enableMicrophone(true) // Enable/disable microphone
|
|
78
|
+
await manager.switchMicrophone(deviceId) // Switch to different audio input
|
|
79
|
+
await manager.getAudioInputDevices() // Get available audio inputs
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Signaling Methods
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const offer = await manager.createOffer()
|
|
86
|
+
const answer = await manager.createAnswer()
|
|
87
|
+
await manager.setLocalDescription(offer)
|
|
88
|
+
await manager.setRemoteDescription(answer)
|
|
89
|
+
await manager.addIceCandidate(candidate)
|
|
90
|
+
await manager.iceRestart() // Trigger ICE restart
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Data Channel Methods
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const dc = manager.createDataChannel(label, options)
|
|
97
|
+
const dc = manager.getDataChannel(label)
|
|
98
|
+
manager.sendData(label, data) // Returns boolean
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Event Subscription
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Subscribe to specific event
|
|
105
|
+
const unsub = manager.on(WebRtcManager.EVENT_STATE_CHANGE, (state) => {
|
|
106
|
+
console.log('State changed:', state);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Subscribe to overall state (Svelte store compatible)
|
|
110
|
+
const unsub = manager.subscribe((state) => {
|
|
111
|
+
console.log('Overall state:', state);
|
|
112
|
+
// state = { state, localStream, remoteStream, dataChannels, peerConnection }
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Available Event Constants:**
|
|
117
|
+
- `EVENT_STATE_CHANGE`
|
|
118
|
+
- `EVENT_LOCAL_STREAM`
|
|
119
|
+
- `EVENT_REMOTE_STREAM`
|
|
120
|
+
- `EVENT_DATA_CHANNEL_OPEN`
|
|
121
|
+
- `EVENT_DATA_CHANNEL_MESSAGE`
|
|
122
|
+
- `EVENT_DATA_CHANNEL_CLOSE`
|
|
123
|
+
- `EVENT_ICE_CANDIDATE`
|
|
124
|
+
- `EVENT_RECONNECTING`
|
|
125
|
+
- `EVENT_RECONNECT_FAILED`
|
|
126
|
+
- `EVENT_DEVICE_CHANGED`
|
|
127
|
+
- `EVENT_MICROPHONE_FAILED`
|
|
128
|
+
- `EVENT_ERROR`
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
|
|
132
|
+
### Basic Usage (Vanilla JavaScript)
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { WebRtcManager, WebRtcState } from '@marianmeres/webrtc';
|
|
136
|
+
|
|
137
|
+
// Create factory (browser implementation)
|
|
138
|
+
const factory = {
|
|
139
|
+
createPeerConnection: (config) => new RTCPeerConnection(config),
|
|
140
|
+
getUserMedia: (constraints) => navigator.mediaDevices.getUserMedia(constraints),
|
|
141
|
+
enumerateDevices: () => navigator.mediaDevices.enumerateDevices(),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Create manager
|
|
145
|
+
const manager = new WebRtcManager(factory, {
|
|
146
|
+
peerConfig: {
|
|
147
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
148
|
+
},
|
|
149
|
+
enableMicrophone: true,
|
|
150
|
+
autoReconnect: true,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Subscribe to events
|
|
154
|
+
manager.on(WebRtcManager.EVENT_ICE_CANDIDATE, (candidate) => {
|
|
155
|
+
// Send candidate to remote peer via your signaling channel
|
|
156
|
+
signalingChannel.send({ type: 'candidate', candidate });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
manager.on(WebRtcManager.EVENT_REMOTE_STREAM, (stream) => {
|
|
160
|
+
// Attach remote stream to audio element
|
|
161
|
+
audioElement.srcObject = stream;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Initialize and create offer
|
|
165
|
+
await manager.initialize();
|
|
166
|
+
await manager.connect();
|
|
167
|
+
const offer = await manager.createOffer();
|
|
168
|
+
await manager.setLocalDescription(offer);
|
|
169
|
+
|
|
170
|
+
// Send offer to remote peer via your signaling channel
|
|
171
|
+
signalingChannel.send({ type: 'offer', offer });
|
|
172
|
+
|
|
173
|
+
// Handle incoming signaling messages
|
|
174
|
+
signalingChannel.onmessage = async (msg) => {
|
|
175
|
+
if (msg.type === 'answer') {
|
|
176
|
+
await manager.setRemoteDescription(msg.answer);
|
|
177
|
+
} else if (msg.type === 'candidate') {
|
|
178
|
+
await manager.addIceCandidate(msg.candidate);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Svelte 5 Integration
|
|
184
|
+
|
|
185
|
+
```svelte
|
|
186
|
+
<script>
|
|
187
|
+
import { WebRtcManager, WebRtcState } from '@marianmeres/webrtc';
|
|
188
|
+
import { onMount } from 'svelte';
|
|
189
|
+
|
|
190
|
+
const factory = {
|
|
191
|
+
createPeerConnection: (config) => new RTCPeerConnection(config),
|
|
192
|
+
getUserMedia: (constraints) => navigator.mediaDevices.getUserMedia(constraints),
|
|
193
|
+
enumerateDevices: () => navigator.mediaDevices.enumerateDevices(),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const manager = new WebRtcManager(factory, {
|
|
197
|
+
peerConfig: {
|
|
198
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
199
|
+
},
|
|
200
|
+
enableMicrophone: true,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Subscribe to overall state (Svelte store compatible!)
|
|
204
|
+
const managerState = $derived(manager.subscribe((state) => state));
|
|
205
|
+
|
|
206
|
+
// Or use individual event subscriptions
|
|
207
|
+
let devices = $state([]);
|
|
208
|
+
|
|
209
|
+
onMount(() => {
|
|
210
|
+
const unsubDevices = manager.on(
|
|
211
|
+
WebRtcManager.EVENT_DEVICE_CHANGED,
|
|
212
|
+
(devs) => devices = devs
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return () => {
|
|
216
|
+
unsubDevices();
|
|
217
|
+
manager.disconnect();
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
async function startCall() {
|
|
222
|
+
await manager.initialize();
|
|
223
|
+
await manager.connect();
|
|
224
|
+
const offer = await manager.createOffer();
|
|
225
|
+
await manager.setLocalDescription(offer);
|
|
226
|
+
// Send offer via your signaling channel
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function switchMic(deviceId) {
|
|
230
|
+
await manager.switchMicrophone(deviceId);
|
|
231
|
+
}
|
|
232
|
+
</script>
|
|
233
|
+
|
|
234
|
+
<div>
|
|
235
|
+
<p>State: {$managerState.state}</p>
|
|
236
|
+
<p>Microphone: {$managerState.localStream ? 'Enabled' : 'Disabled'}</p>
|
|
237
|
+
|
|
238
|
+
<button onclick={startCall}>Start Call</button>
|
|
239
|
+
|
|
240
|
+
<select onchange={(e) => switchMic(e.target.value)}>
|
|
241
|
+
{#each devices as device}
|
|
242
|
+
<option value={device.deviceId}>{device.label}</option>
|
|
243
|
+
{/each}
|
|
244
|
+
</select>
|
|
245
|
+
|
|
246
|
+
<audio bind:this={remoteAudio} autoplay></audio>
|
|
247
|
+
</div>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Complete Peer-to-Peer Example
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { WebRtcManager } from '@marianmores/webrtc';
|
|
254
|
+
|
|
255
|
+
class P2PConnection {
|
|
256
|
+
manager: WebRtcManager;
|
|
257
|
+
signalingChannel: WebSocket;
|
|
258
|
+
|
|
259
|
+
constructor(signalingUrl: string) {
|
|
260
|
+
this.manager = new WebRtcManager(
|
|
261
|
+
{
|
|
262
|
+
createPeerConnection: (config) => new RTCPeerConnection(config),
|
|
263
|
+
getUserMedia: (constraints) => navigator.mediaDevices.getUserMedia(constraints),
|
|
264
|
+
enumerateDevices: () => navigator.mediaDevices.enumerateDevices(),
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
peerConfig: {
|
|
268
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
269
|
+
},
|
|
270
|
+
enableMicrophone: true,
|
|
271
|
+
dataChannelLabel: 'chat',
|
|
272
|
+
autoReconnect: true,
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
this.signalingChannel = new WebSocket(signalingUrl);
|
|
277
|
+
this.setupSignaling();
|
|
278
|
+
this.setupManagerEvents();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
setupSignaling() {
|
|
282
|
+
this.signalingChannel.onmessage = async (event) => {
|
|
283
|
+
const msg = JSON.parse(event.data);
|
|
284
|
+
|
|
285
|
+
switch (msg.type) {
|
|
286
|
+
case 'offer':
|
|
287
|
+
await this.handleOffer(msg.offer);
|
|
288
|
+
break;
|
|
289
|
+
case 'answer':
|
|
290
|
+
await this.manager.setRemoteDescription(msg.answer);
|
|
291
|
+
break;
|
|
292
|
+
case 'candidate':
|
|
293
|
+
await this.manager.addIceCandidate(msg.candidate);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
setupManagerEvents() {
|
|
300
|
+
// Send ICE candidates to remote peer
|
|
301
|
+
this.manager.on(WebRtcManager.EVENT_ICE_CANDIDATE, (candidate) => {
|
|
302
|
+
this.signalingChannel.send(JSON.stringify({
|
|
303
|
+
type: 'candidate',
|
|
304
|
+
candidate,
|
|
305
|
+
}));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Handle remote audio stream
|
|
309
|
+
this.manager.on(WebRtcManager.EVENT_REMOTE_STREAM, (stream) => {
|
|
310
|
+
const audio = document.getElementById('remote-audio') as HTMLAudioElement;
|
|
311
|
+
audio.srcObject = stream;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Handle data channel messages
|
|
315
|
+
this.manager.on(WebRtcManager.EVENT_DATA_CHANNEL_MESSAGE, ({ data }) => {
|
|
316
|
+
console.log('Received message:', data);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Handle reconnection
|
|
320
|
+
this.manager.on(WebRtcManager.EVENT_RECONNECTING, ({ attempt, strategy }) => {
|
|
321
|
+
console.log(`Reconnecting (attempt ${attempt}, strategy: ${strategy})`);
|
|
322
|
+
if (strategy === 'full') {
|
|
323
|
+
// For full reconnection, we need to re-do the signaling handshake
|
|
324
|
+
this.createOffer();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async createOffer() {
|
|
330
|
+
await this.manager.initialize();
|
|
331
|
+
await this.manager.connect();
|
|
332
|
+
const offer = await this.manager.createOffer();
|
|
333
|
+
await this.manager.setLocalDescription(offer);
|
|
334
|
+
|
|
335
|
+
this.signalingChannel.send(JSON.stringify({
|
|
336
|
+
type: 'offer',
|
|
337
|
+
offer,
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async handleOffer(offer: RTCSessionDescriptionInit) {
|
|
342
|
+
await this.manager.initialize();
|
|
343
|
+
await this.manager.setRemoteDescription(offer);
|
|
344
|
+
const answer = await this.manager.createAnswer();
|
|
345
|
+
await this.manager.setLocalDescription(answer);
|
|
346
|
+
|
|
347
|
+
this.signalingChannel.send(JSON.stringify({
|
|
348
|
+
type: 'answer',
|
|
349
|
+
answer,
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
sendMessage(text: string) {
|
|
354
|
+
this.manager.sendData('chat', text);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
disconnect() {
|
|
358
|
+
this.manager.disconnect();
|
|
359
|
+
this.signalingChannel.close();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Usage
|
|
364
|
+
const connection = new P2PConnection('wss://your-signaling-server.com');
|
|
365
|
+
await connection.createOffer();
|
|
366
|
+
connection.sendMessage('Hello!');
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## State Machine
|
|
370
|
+
|
|
371
|
+
The manager uses a finite state machine with the following states:
|
|
372
|
+
|
|
373
|
+

|
|
374
|
+
|
|
375
|
+
## Testing
|
|
376
|
+
|
|
377
|
+
The project includes two types of tests:
|
|
378
|
+
|
|
379
|
+
### Unit Tests (Deno)
|
|
380
|
+
|
|
381
|
+
Mock-based tests for the manager's logic and state transitions:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
deno task test
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Browser Integration Tests
|
|
388
|
+
|
|
389
|
+
Real peer-to-peer connection tests running in a browser environment:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
deno task test:browser
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
This builds the test bundle and starts a local server. Open the provided URL in your browser to run the tests interactively.
|
|
396
|
+
|
|
397
|
+
The browser tests verify:
|
|
398
|
+
- Actual P2P connections between peers
|
|
399
|
+
- Data channel message exchange
|
|
400
|
+
- ICE candidate exchange
|
|
401
|
+
- Connection state transitions
|
|
402
|
+
- Resource cleanup
|
|
403
|
+
|
|
404
|
+
See [tests/browser/README.md](tests/browser/README.md) for more details.
|
|
405
|
+
|
|
406
|
+
## License
|
|
407
|
+
|
|
408
|
+
MIT
|
package/dist/mod.d.ts
ADDED
package/dist/mod.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface WebRtcManagerConfig {
|
|
2
|
+
/** Initial peer configuration (ICE servers, etc.) */
|
|
3
|
+
peerConfig?: RTCConfiguration;
|
|
4
|
+
/** Whether to enable microphone initially. Defaults to false. */
|
|
5
|
+
enableMicrophone?: boolean;
|
|
6
|
+
/** Label for the default data channel. If provided, a data channel will be created on connect. */
|
|
7
|
+
dataChannelLabel?: string;
|
|
8
|
+
/** Enable automatic reconnection on connection failure. Defaults to false. */
|
|
9
|
+
autoReconnect?: boolean;
|
|
10
|
+
/** Maximum number of reconnection attempts. Defaults to 5. */
|
|
11
|
+
maxReconnectAttempts?: number;
|
|
12
|
+
/** Initial reconnection delay in ms. Doubles with each attempt. Defaults to 1000. */
|
|
13
|
+
reconnectDelay?: number;
|
|
14
|
+
/** Debug mode for logging */
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface WebRtcFactory {
|
|
18
|
+
createPeerConnection(config?: RTCConfiguration): RTCPeerConnection;
|
|
19
|
+
getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
|
20
|
+
enumerateDevices(): Promise<MediaDeviceInfo[]>;
|
|
21
|
+
}
|
|
22
|
+
export declare enum WebRtcState {
|
|
23
|
+
IDLE = "IDLE",
|
|
24
|
+
INITIALIZING = "INITIALIZING",
|
|
25
|
+
CONNECTING = "CONNECTING",
|
|
26
|
+
CONNECTED = "CONNECTED",
|
|
27
|
+
RECONNECTING = "RECONNECTING",
|
|
28
|
+
DISCONNECTED = "DISCONNECTED",
|
|
29
|
+
ERROR = "ERROR"
|
|
30
|
+
}
|
|
31
|
+
export declare enum WebRtcFsmEvent {
|
|
32
|
+
INIT = "initialize",
|
|
33
|
+
CONNECT = "connect",
|
|
34
|
+
CONNECTED = "connected",
|
|
35
|
+
RECONNECTING = "reconnecting",
|
|
36
|
+
DISCONNECT = "disconnect",
|
|
37
|
+
ERROR = "error",
|
|
38
|
+
RESET = "reset"
|
|
39
|
+
}
|
|
40
|
+
export interface WebRtcEvents {
|
|
41
|
+
state_change: WebRtcState;
|
|
42
|
+
local_stream: MediaStream | null;
|
|
43
|
+
remote_stream: MediaStream | null;
|
|
44
|
+
data_channel_open: RTCDataChannel;
|
|
45
|
+
data_channel_message: {
|
|
46
|
+
channel: RTCDataChannel;
|
|
47
|
+
data: any;
|
|
48
|
+
};
|
|
49
|
+
data_channel_close: RTCDataChannel;
|
|
50
|
+
ice_candidate: RTCIceCandidate | null;
|
|
51
|
+
/**
|
|
52
|
+
* Emitted when reconnection is being attempted.
|
|
53
|
+
* For 'full' strategy reconnections, consumers should listen for this event
|
|
54
|
+
* and re-establish signaling (create new offer/answer exchange).
|
|
55
|
+
* The manager will call connect() but cannot handle the signaling automatically.
|
|
56
|
+
*/
|
|
57
|
+
reconnecting: {
|
|
58
|
+
attempt: number;
|
|
59
|
+
strategy: "ice-restart" | "full";
|
|
60
|
+
};
|
|
61
|
+
reconnect_failed: {
|
|
62
|
+
attempts: number;
|
|
63
|
+
};
|
|
64
|
+
device_changed: MediaDeviceInfo[];
|
|
65
|
+
microphone_failed: {
|
|
66
|
+
error?: any;
|
|
67
|
+
reason?: string;
|
|
68
|
+
};
|
|
69
|
+
error: Error;
|
|
70
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export var WebRtcState;
|
|
2
|
+
(function (WebRtcState) {
|
|
3
|
+
WebRtcState["IDLE"] = "IDLE";
|
|
4
|
+
WebRtcState["INITIALIZING"] = "INITIALIZING";
|
|
5
|
+
WebRtcState["CONNECTING"] = "CONNECTING";
|
|
6
|
+
WebRtcState["CONNECTED"] = "CONNECTED";
|
|
7
|
+
WebRtcState["RECONNECTING"] = "RECONNECTING";
|
|
8
|
+
WebRtcState["DISCONNECTED"] = "DISCONNECTED";
|
|
9
|
+
WebRtcState["ERROR"] = "ERROR";
|
|
10
|
+
})(WebRtcState || (WebRtcState = {}));
|
|
11
|
+
export var WebRtcFsmEvent;
|
|
12
|
+
(function (WebRtcFsmEvent) {
|
|
13
|
+
WebRtcFsmEvent["INIT"] = "initialize";
|
|
14
|
+
WebRtcFsmEvent["CONNECT"] = "connect";
|
|
15
|
+
WebRtcFsmEvent["CONNECTED"] = "connected";
|
|
16
|
+
WebRtcFsmEvent["RECONNECTING"] = "reconnecting";
|
|
17
|
+
WebRtcFsmEvent["DISCONNECT"] = "disconnect";
|
|
18
|
+
WebRtcFsmEvent["ERROR"] = "error";
|
|
19
|
+
WebRtcFsmEvent["RESET"] = "reset";
|
|
20
|
+
})(WebRtcFsmEvent || (WebRtcFsmEvent = {}));
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { type WebRtcFactory, type WebRtcManagerConfig, WebRtcState, type WebRtcEvents } from "./types.js";
|
|
2
|
+
export declare class WebRtcManager {
|
|
3
|
+
#private;
|
|
4
|
+
static readonly EVENT_STATE_CHANGE = "state_change";
|
|
5
|
+
static readonly EVENT_LOCAL_STREAM = "local_stream";
|
|
6
|
+
static readonly EVENT_REMOTE_STREAM = "remote_stream";
|
|
7
|
+
static readonly EVENT_DATA_CHANNEL_OPEN = "data_channel_open";
|
|
8
|
+
static readonly EVENT_DATA_CHANNEL_MESSAGE = "data_channel_message";
|
|
9
|
+
static readonly EVENT_DATA_CHANNEL_CLOSE = "data_channel_close";
|
|
10
|
+
static readonly EVENT_ICE_CANDIDATE = "ice_candidate";
|
|
11
|
+
static readonly EVENT_RECONNECTING = "reconnecting";
|
|
12
|
+
static readonly EVENT_RECONNECT_FAILED = "reconnect_failed";
|
|
13
|
+
static readonly EVENT_DEVICE_CHANGED = "device_changed";
|
|
14
|
+
static readonly EVENT_MICROPHONE_FAILED = "microphone_failed";
|
|
15
|
+
static readonly EVENT_ERROR = "error";
|
|
16
|
+
constructor(factory: WebRtcFactory, config?: WebRtcManagerConfig);
|
|
17
|
+
/** Returns the current state of the WebRTC connection. */
|
|
18
|
+
get state(): WebRtcState;
|
|
19
|
+
/** Returns a readonly map of all active data channels indexed by label. */
|
|
20
|
+
get dataChannels(): ReadonlyMap<string, RTCDataChannel>;
|
|
21
|
+
/** Returns the local media stream, or null if not initialized. */
|
|
22
|
+
get localStream(): MediaStream | null;
|
|
23
|
+
/** Returns the remote media stream, or null if not connected. */
|
|
24
|
+
get remoteStream(): MediaStream | null;
|
|
25
|
+
/** Returns the underlying RTCPeerConnection, or null if not initialized. */
|
|
26
|
+
get peerConnection(): RTCPeerConnection | null;
|
|
27
|
+
/** Returns a Mermaid diagram representation of the FSM state machine. */
|
|
28
|
+
toMermaid(): string;
|
|
29
|
+
/**
|
|
30
|
+
* Subscribe to a specific WebRTC event.
|
|
31
|
+
* @returns Unsubscribe function to remove the event listener.
|
|
32
|
+
*/
|
|
33
|
+
on(event: keyof WebRtcEvents, handler: (data: any) => void): () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to the overall state of the WebRTC manager.
|
|
36
|
+
* Compatible with Svelte stores - immediately calls handler with current state,
|
|
37
|
+
* then notifies on any changes to state, streams, or data channels.
|
|
38
|
+
* @param handler - Callback that receives the overall state object
|
|
39
|
+
* @returns Unsubscribe function to remove the event listener.
|
|
40
|
+
*/
|
|
41
|
+
subscribe(handler: (state: {
|
|
42
|
+
state: WebRtcState;
|
|
43
|
+
localStream: MediaStream | null;
|
|
44
|
+
remoteStream: MediaStream | null;
|
|
45
|
+
dataChannels: ReadonlyMap<string, RTCDataChannel>;
|
|
46
|
+
peerConnection: RTCPeerConnection | null;
|
|
47
|
+
}) => void): () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Retrieves all available audio input devices.
|
|
50
|
+
* @returns Array of audio input devices, or empty array on error.
|
|
51
|
+
*/
|
|
52
|
+
getAudioInputDevices(): Promise<MediaDeviceInfo[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Switches the active microphone to a different audio input device.
|
|
55
|
+
* @param deviceId - The device ID of the audio input to switch to.
|
|
56
|
+
* @returns True if the switch was successful, false otherwise.
|
|
57
|
+
*/
|
|
58
|
+
switchMicrophone(deviceId: string): Promise<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Initializes the WebRTC peer connection and sets up media tracks.
|
|
61
|
+
* Must be called before creating offers or answers. Can only be called from IDLE state.
|
|
62
|
+
*/
|
|
63
|
+
initialize(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Transitions to the CONNECTING state. Automatically initializes if needed.
|
|
66
|
+
* If disconnected, reinitializes the peer connection.
|
|
67
|
+
*/
|
|
68
|
+
connect(): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Enables or disables the microphone and adds/removes audio tracks to the peer connection.
|
|
71
|
+
* @param enable - True to enable microphone, false to disable.
|
|
72
|
+
* @returns True if successful, false if failed to get user media.
|
|
73
|
+
*/
|
|
74
|
+
enableMicrophone(enable: boolean): Promise<boolean>;
|
|
75
|
+
/**
|
|
76
|
+
* Disconnects the peer connection and cleans up all resources.
|
|
77
|
+
* Transitions to DISCONNECTED state.
|
|
78
|
+
*/
|
|
79
|
+
disconnect(): void;
|
|
80
|
+
/**
|
|
81
|
+
* Resets the manager to IDLE state from any state.
|
|
82
|
+
* Cleans up all resources and allows reinitialization.
|
|
83
|
+
*/
|
|
84
|
+
reset(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Creates a new data channel with the specified label.
|
|
87
|
+
* Returns existing channel if one with the same label already exists.
|
|
88
|
+
* @param label - The label for the data channel.
|
|
89
|
+
* @param options - Optional RTCDataChannelInit configuration.
|
|
90
|
+
* @returns The created data channel, or null if peer connection not initialized.
|
|
91
|
+
*/
|
|
92
|
+
createDataChannel(label: string, options?: RTCDataChannelInit): RTCDataChannel | null;
|
|
93
|
+
/**
|
|
94
|
+
* Retrieves an existing data channel by label.
|
|
95
|
+
* @param label - The label of the data channel to retrieve.
|
|
96
|
+
* @returns The data channel if found, undefined otherwise.
|
|
97
|
+
*/
|
|
98
|
+
getDataChannel(label: string): RTCDataChannel | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Sends data through a data channel identified by label.
|
|
101
|
+
* Checks that the channel exists and is in open state before sending.
|
|
102
|
+
* @param label - The label of the data channel to send through.
|
|
103
|
+
* @param data - The data to send (string, Blob, or ArrayBuffer).
|
|
104
|
+
* @returns True if data was sent successfully, false otherwise.
|
|
105
|
+
*/
|
|
106
|
+
sendData(label: string, data: string | Blob | ArrayBuffer | ArrayBufferView<ArrayBuffer>): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Creates an SDP offer for initiating a WebRTC connection.
|
|
109
|
+
* @param options - Optional offer configuration.
|
|
110
|
+
* @returns The offer SDP, or null if peer connection not initialized.
|
|
111
|
+
*/
|
|
112
|
+
createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit | null>;
|
|
113
|
+
/**
|
|
114
|
+
* Creates an SDP answer in response to a received offer.
|
|
115
|
+
* @param options - Optional answer configuration.
|
|
116
|
+
* @returns The answer SDP, or null if peer connection not initialized.
|
|
117
|
+
*/
|
|
118
|
+
createAnswer(options?: RTCAnswerOptions): Promise<RTCSessionDescriptionInit | null>;
|
|
119
|
+
/**
|
|
120
|
+
* Sets the local description for the peer connection.
|
|
121
|
+
* @param description - The SDP description (offer or answer).
|
|
122
|
+
* @returns True if successful, false otherwise.
|
|
123
|
+
*/
|
|
124
|
+
setLocalDescription(description: RTCSessionDescriptionInit): Promise<boolean>;
|
|
125
|
+
/**
|
|
126
|
+
* Sets the remote description received from the peer.
|
|
127
|
+
* @param description - The remote SDP description.
|
|
128
|
+
* @returns True if successful, false otherwise.
|
|
129
|
+
*/
|
|
130
|
+
setRemoteDescription(description: RTCSessionDescriptionInit): Promise<boolean>;
|
|
131
|
+
/**
|
|
132
|
+
* Adds an ICE candidate received from the remote peer.
|
|
133
|
+
* @param candidate - The ICE candidate to add, or null for end-of-candidates.
|
|
134
|
+
* @returns True if successful, false otherwise.
|
|
135
|
+
*/
|
|
136
|
+
addIceCandidate(candidate: RTCIceCandidateInit | null): Promise<boolean>;
|
|
137
|
+
/**
|
|
138
|
+
* Performs an ICE restart to recover from connection issues.
|
|
139
|
+
* Creates a new offer with iceRestart flag and sets it as local description.
|
|
140
|
+
* @returns True if successful, false otherwise.
|
|
141
|
+
*/
|
|
142
|
+
iceRestart(): Promise<boolean>;
|
|
143
|
+
/**
|
|
144
|
+
* Returns the current local session description.
|
|
145
|
+
* @returns The local description, or null if not set.
|
|
146
|
+
*/
|
|
147
|
+
getLocalDescription(): RTCSessionDescription | null;
|
|
148
|
+
/**
|
|
149
|
+
* Returns the current remote session description.
|
|
150
|
+
* @returns The remote description, or null if not set.
|
|
151
|
+
*/
|
|
152
|
+
getRemoteDescription(): RTCSessionDescription | null;
|
|
153
|
+
/**
|
|
154
|
+
* Retrieves WebRTC statistics for the peer connection.
|
|
155
|
+
* @returns Stats report, or null if peer connection not initialized.
|
|
156
|
+
*/
|
|
157
|
+
getStats(): Promise<RTCStatsReport | null>;
|
|
158
|
+
}
|