@xtr-dev/rondevu-client 0.0.2 → 0.0.4
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 +37 -211
- package/dist/client.d.ts +4 -4
- package/dist/client.js +5 -9
- package/dist/connection.d.ts +75 -0
- package/dist/connection.js +260 -0
- package/dist/event-emitter.d.ts +31 -0
- package/dist/event-emitter.js +78 -0
- package/dist/index.d.ts +5 -27
- package/dist/index.js +8 -46
- package/dist/rondevu.d.ts +58 -0
- package/dist/rondevu.js +204 -0
- package/dist/types.d.ts +62 -2
- package/dist/types.js +4 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,234 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Rondevu
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
🎯 Meet WebRTC peers by topic, by peer ID, or by connection ID.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## @xtr-dev/rondevu-client
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
npm install @xtr-dev/rondevu-client
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
### Basic Setup
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
import { RondevuClient } from '@xtr-dev/rondevu-client';
|
|
17
|
-
|
|
18
|
-
const client = new RondevuClient({
|
|
19
|
-
baseUrl: 'https://rondevu.example.com',
|
|
20
|
-
// Optional: custom origin for session isolation
|
|
21
|
-
origin: 'https://myapp.com'
|
|
22
|
-
});
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
### Peer Discovery Flow
|
|
26
|
-
|
|
27
|
-
#### 1. List Available Topics
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
// Get all topics with peer counts
|
|
31
|
-
const { topics, pagination } = await client.listTopics();
|
|
32
|
-
|
|
33
|
-
topics.forEach(topic => {
|
|
34
|
-
console.log(`${topic.topic}: ${topic.count} peers available`);
|
|
35
|
-
});
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
#### 2. Create an Offer (Peer A)
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
// Announce availability in a topic
|
|
42
|
-
const { code } = await client.createOffer('my-room', {
|
|
43
|
-
info: 'peer-A-unique-id',
|
|
44
|
-
offer: webrtcOfferData
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
console.log('Session code:', code);
|
|
48
|
-
```
|
|
7
|
+
[](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
|
|
49
8
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
// Find available peers in a topic
|
|
54
|
-
const { sessions } = await client.listSessions('my-room');
|
|
9
|
+
TypeScript Rondevu HTTP and WebRTC client, for simple peer discovery and connection.
|
|
55
10
|
|
|
56
|
-
|
|
57
|
-
const otherPeers = sessions.filter(s => s.info !== 'my-peer-id');
|
|
11
|
+
### Install
|
|
58
12
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
console.log('Found peer:', peer.info);
|
|
62
|
-
}
|
|
13
|
+
```bash
|
|
14
|
+
npm install @xtr-dev/rondevu-client
|
|
63
15
|
```
|
|
64
16
|
|
|
65
|
-
|
|
17
|
+
### Usage
|
|
66
18
|
|
|
67
19
|
```typescript
|
|
68
|
-
|
|
69
|
-
await client.sendAnswer({
|
|
70
|
-
code: peer.code,
|
|
71
|
-
answer: webrtcAnswerData,
|
|
72
|
-
side: 'answerer'
|
|
73
|
-
});
|
|
74
|
-
```
|
|
20
|
+
import { Rondevu } from '@xtr-dev/rondevu-client';
|
|
75
21
|
|
|
76
|
-
|
|
22
|
+
const rdv = new Rondevu({ baseUrl: 'https://server.com' });
|
|
77
23
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const offererData = await client.poll(code, 'offerer');
|
|
81
|
-
if (offererData.answer) {
|
|
82
|
-
console.log('Received answer from peer');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Answerer polls for offer details
|
|
86
|
-
const answererData = await client.poll(code, 'answerer');
|
|
87
|
-
console.log('Offer candidates:', answererData.offerCandidates);
|
|
88
|
-
```
|
|
24
|
+
// Connect by topic
|
|
25
|
+
const conn = await rdv.join('room');
|
|
89
26
|
|
|
90
|
-
|
|
27
|
+
// Or connect by ID
|
|
28
|
+
const conn = await rdv.connect('meeting-123');
|
|
91
29
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
candidate: iceCandidate,
|
|
97
|
-
side: 'offerer' // or 'answerer'
|
|
30
|
+
// Use the connection
|
|
31
|
+
conn.on('connect', () => {
|
|
32
|
+
const channel = conn.dataChannel('chat');
|
|
33
|
+
channel.send('Hello!');
|
|
98
34
|
});
|
|
99
35
|
```
|
|
100
36
|
|
|
101
|
-
###
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
const health = await client.health();
|
|
105
|
-
console.log('Server status:', health.status);
|
|
106
|
-
console.log('Timestamp:', health.timestamp);
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## API Reference
|
|
110
|
-
|
|
111
|
-
### `RondevuClient`
|
|
112
|
-
|
|
113
|
-
#### Constructor
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
new RondevuClient(options: RondevuClientOptions)
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
**Options:**
|
|
120
|
-
- `baseUrl` (string, required): Base URL of the Rondevu server
|
|
121
|
-
- `origin` (string, optional): Origin header for session isolation (defaults to baseUrl origin)
|
|
122
|
-
- `fetch` (function, optional): Custom fetch implementation (for Node.js)
|
|
123
|
-
|
|
124
|
-
#### Methods
|
|
125
|
-
|
|
126
|
-
##### `listTopics(page?, limit?)`
|
|
127
|
-
|
|
128
|
-
Lists all topics with peer counts.
|
|
129
|
-
|
|
130
|
-
**Parameters:**
|
|
131
|
-
- `page` (number, optional): Page number, default 1
|
|
132
|
-
- `limit` (number, optional): Results per page, default 100, max 1000
|
|
133
|
-
|
|
134
|
-
**Returns:** `Promise<ListTopicsResponse>`
|
|
135
|
-
|
|
136
|
-
##### `listSessions(topic)`
|
|
137
|
-
|
|
138
|
-
Discovers available peers for a given topic.
|
|
139
|
-
|
|
140
|
-
**Parameters:**
|
|
141
|
-
- `topic` (string): Topic identifier
|
|
142
|
-
|
|
143
|
-
**Returns:** `Promise<ListSessionsResponse>`
|
|
144
|
-
|
|
145
|
-
##### `createOffer(topic, request)`
|
|
146
|
-
|
|
147
|
-
Announces peer availability and creates a new session.
|
|
148
|
-
|
|
149
|
-
**Parameters:**
|
|
150
|
-
- `topic` (string): Topic identifier (max 256 characters)
|
|
151
|
-
- `request` (CreateOfferRequest):
|
|
152
|
-
- `info` (string): Peer identifier/metadata (max 1024 characters)
|
|
153
|
-
- `offer` (string): WebRTC signaling data
|
|
154
|
-
|
|
155
|
-
**Returns:** `Promise<CreateOfferResponse>`
|
|
156
|
-
|
|
157
|
-
##### `sendAnswer(request)`
|
|
158
|
-
|
|
159
|
-
Sends an answer or candidate to an existing session.
|
|
160
|
-
|
|
161
|
-
**Parameters:**
|
|
162
|
-
- `request` (AnswerRequest):
|
|
163
|
-
- `code` (string): Session UUID
|
|
164
|
-
- `answer` (string, optional): Answer signaling data
|
|
165
|
-
- `candidate` (string, optional): ICE candidate data
|
|
166
|
-
- `side` ('offerer' | 'answerer'): Which peer is sending
|
|
167
|
-
|
|
168
|
-
**Returns:** `Promise<AnswerResponse>`
|
|
169
|
-
|
|
170
|
-
##### `poll(code, side)`
|
|
171
|
-
|
|
172
|
-
Polls for session data from the other peer.
|
|
173
|
-
|
|
174
|
-
**Parameters:**
|
|
175
|
-
- `code` (string): Session UUID
|
|
176
|
-
- `side` ('offerer' | 'answerer'): Which side is polling
|
|
177
|
-
|
|
178
|
-
**Returns:** `Promise<PollOffererResponse | PollAnswererResponse>`
|
|
179
|
-
|
|
180
|
-
##### `health()`
|
|
181
|
-
|
|
182
|
-
Checks server health.
|
|
183
|
-
|
|
184
|
-
**Returns:** `Promise<HealthResponse>`
|
|
185
|
-
|
|
186
|
-
## TypeScript Types
|
|
187
|
-
|
|
188
|
-
All types are exported from the main package:
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
import {
|
|
192
|
-
RondevuClient,
|
|
193
|
-
Session,
|
|
194
|
-
TopicInfo,
|
|
195
|
-
CreateOfferRequest,
|
|
196
|
-
AnswerRequest,
|
|
197
|
-
PollRequest,
|
|
198
|
-
Side,
|
|
199
|
-
// ... and more
|
|
200
|
-
} from '@xtr-dev/rondevu-client';
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## Node.js Usage
|
|
204
|
-
|
|
205
|
-
For Node.js environments (v18+), the built-in fetch is used automatically. For older Node.js versions, provide a fetch implementation:
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
import fetch from 'node-fetch';
|
|
209
|
-
import { RondevuClient } from '@xtr-dev/rondevu-client';
|
|
210
|
-
|
|
211
|
-
const client = new RondevuClient({
|
|
212
|
-
baseUrl: 'https://rondevu.example.com',
|
|
213
|
-
fetch: fetch as any
|
|
214
|
-
});
|
|
215
|
-
```
|
|
37
|
+
### API
|
|
216
38
|
|
|
217
|
-
|
|
39
|
+
**Main Methods:**
|
|
40
|
+
- `rdv.join(topic)` - Auto-connect to first peer in topic
|
|
41
|
+
- `rdv.join(topic, {filter})` - Connect to specific peer by ID
|
|
42
|
+
- `rdv.create(id, topic)` - Create connection for others to join
|
|
43
|
+
- `rdv.connect(id)` - Join connection by ID
|
|
218
44
|
|
|
219
|
-
|
|
45
|
+
**Connection Events:**
|
|
46
|
+
- `connect` - Connection established
|
|
47
|
+
- `disconnect` - Connection closed
|
|
48
|
+
- `datachannel` - Remote peer created data channel
|
|
49
|
+
- `stream` - Remote media stream received
|
|
50
|
+
- `error` - Error occurred
|
|
220
51
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
});
|
|
227
|
-
} catch (error) {
|
|
228
|
-
console.error('Failed to create offer:', error.message);
|
|
229
|
-
}
|
|
230
|
-
```
|
|
52
|
+
**Connection Methods:**
|
|
53
|
+
- `conn.dataChannel(label)` - Get or create data channel
|
|
54
|
+
- `conn.addStream(stream)` - Add media stream
|
|
55
|
+
- `conn.getPeerConnection()` - Get underlying RTCPeerConnection
|
|
56
|
+
- `conn.close()` - Close connection
|
|
231
57
|
|
|
232
|
-
|
|
58
|
+
### License
|
|
233
59
|
|
|
234
60
|
MIT
|
package/dist/client.d.ts
CHANGED
|
@@ -40,22 +40,22 @@ export declare class RondevuClient {
|
|
|
40
40
|
* ```typescript
|
|
41
41
|
* const client = new RondevuClient({ baseUrl: 'https://example.com' });
|
|
42
42
|
* const { sessions } = await client.listSessions('my-room');
|
|
43
|
-
* const otherPeers = sessions.filter(s => s.
|
|
43
|
+
* const otherPeers = sessions.filter(s => s.peerId !== myPeerId);
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
listSessions(topic: string): Promise<ListSessionsResponse>;
|
|
47
47
|
/**
|
|
48
48
|
* Announces peer availability and creates a new session
|
|
49
49
|
*
|
|
50
|
-
* @param topic - Topic identifier for grouping peers (max
|
|
51
|
-
* @param request - Offer details including peer
|
|
50
|
+
* @param topic - Topic identifier for grouping peers (max 1024 characters)
|
|
51
|
+
* @param request - Offer details including peer ID and signaling data
|
|
52
52
|
* @returns Unique session code (UUID)
|
|
53
53
|
*
|
|
54
54
|
* @example
|
|
55
55
|
* ```typescript
|
|
56
56
|
* const client = new RondevuClient({ baseUrl: 'https://example.com' });
|
|
57
57
|
* const { code } = await client.createOffer('my-room', {
|
|
58
|
-
*
|
|
58
|
+
* peerId: 'peer-123',
|
|
59
59
|
* offer: signalingData
|
|
60
60
|
* });
|
|
61
61
|
* console.log('Session code:', code);
|
package/dist/client.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RondevuClient = void 0;
|
|
4
1
|
/**
|
|
5
2
|
* HTTP client for Rondevu peer signaling and discovery server
|
|
6
3
|
*/
|
|
7
|
-
class RondevuClient {
|
|
4
|
+
export class RondevuClient {
|
|
8
5
|
/**
|
|
9
6
|
* Creates a new Rondevu client instance
|
|
10
7
|
* @param options - Client configuration options
|
|
@@ -70,7 +67,7 @@ class RondevuClient {
|
|
|
70
67
|
* ```typescript
|
|
71
68
|
* const client = new RondevuClient({ baseUrl: 'https://example.com' });
|
|
72
69
|
* const { sessions } = await client.listSessions('my-room');
|
|
73
|
-
* const otherPeers = sessions.filter(s => s.
|
|
70
|
+
* const otherPeers = sessions.filter(s => s.peerId !== myPeerId);
|
|
74
71
|
* ```
|
|
75
72
|
*/
|
|
76
73
|
async listSessions(topic) {
|
|
@@ -81,15 +78,15 @@ class RondevuClient {
|
|
|
81
78
|
/**
|
|
82
79
|
* Announces peer availability and creates a new session
|
|
83
80
|
*
|
|
84
|
-
* @param topic - Topic identifier for grouping peers (max
|
|
85
|
-
* @param request - Offer details including peer
|
|
81
|
+
* @param topic - Topic identifier for grouping peers (max 1024 characters)
|
|
82
|
+
* @param request - Offer details including peer ID and signaling data
|
|
86
83
|
* @returns Unique session code (UUID)
|
|
87
84
|
*
|
|
88
85
|
* @example
|
|
89
86
|
* ```typescript
|
|
90
87
|
* const client = new RondevuClient({ baseUrl: 'https://example.com' });
|
|
91
88
|
* const { code } = await client.createOffer('my-room', {
|
|
92
|
-
*
|
|
89
|
+
* peerId: 'peer-123',
|
|
93
90
|
* offer: signalingData
|
|
94
91
|
* });
|
|
95
92
|
* console.log('Session code:', code);
|
|
@@ -179,4 +176,3 @@ class RondevuClient {
|
|
|
179
176
|
});
|
|
180
177
|
}
|
|
181
178
|
}
|
|
182
|
-
exports.RondevuClient = RondevuClient;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { EventEmitter } from './event-emitter';
|
|
2
|
+
import { RondevuClient } from './client';
|
|
3
|
+
import { RondevuConnectionParams } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Represents a WebRTC connection with automatic signaling and ICE exchange
|
|
6
|
+
*/
|
|
7
|
+
export declare class RondevuConnection extends EventEmitter {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly topic: string;
|
|
10
|
+
readonly role: 'offerer' | 'answerer';
|
|
11
|
+
readonly remotePeerId: string;
|
|
12
|
+
private pc;
|
|
13
|
+
private client;
|
|
14
|
+
private localPeerId;
|
|
15
|
+
private dataChannels;
|
|
16
|
+
private pollingInterval?;
|
|
17
|
+
private pollingIntervalMs;
|
|
18
|
+
private connectionTimeoutMs;
|
|
19
|
+
private connectionTimer?;
|
|
20
|
+
private isPolling;
|
|
21
|
+
private isClosed;
|
|
22
|
+
constructor(params: RondevuConnectionParams, client: RondevuClient);
|
|
23
|
+
/**
|
|
24
|
+
* Setup RTCPeerConnection event handlers
|
|
25
|
+
*/
|
|
26
|
+
private setupEventHandlers;
|
|
27
|
+
/**
|
|
28
|
+
* Handle RTCPeerConnection state changes
|
|
29
|
+
*/
|
|
30
|
+
private handleConnectionStateChange;
|
|
31
|
+
/**
|
|
32
|
+
* Send an ICE candidate to the remote peer via signaling server
|
|
33
|
+
*/
|
|
34
|
+
private sendIceCandidate;
|
|
35
|
+
/**
|
|
36
|
+
* Start polling for remote session data (answer/candidates)
|
|
37
|
+
*/
|
|
38
|
+
startPolling(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Stop polling
|
|
41
|
+
*/
|
|
42
|
+
private stopPolling;
|
|
43
|
+
/**
|
|
44
|
+
* Poll the signaling server for remote data
|
|
45
|
+
*/
|
|
46
|
+
private poll;
|
|
47
|
+
/**
|
|
48
|
+
* Handle remotely created data channel
|
|
49
|
+
*/
|
|
50
|
+
private handleRemoteDataChannel;
|
|
51
|
+
/**
|
|
52
|
+
* Get or create a data channel
|
|
53
|
+
*/
|
|
54
|
+
dataChannel(label: string, options?: RTCDataChannelInit): RTCDataChannel;
|
|
55
|
+
/**
|
|
56
|
+
* Add a local media stream to the connection
|
|
57
|
+
*/
|
|
58
|
+
addStream(stream: MediaStream): void;
|
|
59
|
+
/**
|
|
60
|
+
* Get the underlying RTCPeerConnection for advanced usage
|
|
61
|
+
*/
|
|
62
|
+
getPeerConnection(): RTCPeerConnection;
|
|
63
|
+
/**
|
|
64
|
+
* Start connection timeout
|
|
65
|
+
*/
|
|
66
|
+
private startConnectionTimeout;
|
|
67
|
+
/**
|
|
68
|
+
* Clear connection timeout
|
|
69
|
+
*/
|
|
70
|
+
private clearConnectionTimeout;
|
|
71
|
+
/**
|
|
72
|
+
* Close the connection and cleanup resources
|
|
73
|
+
*/
|
|
74
|
+
close(): void;
|
|
75
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { EventEmitter } from './event-emitter';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a WebRTC connection with automatic signaling and ICE exchange
|
|
4
|
+
*/
|
|
5
|
+
export class RondevuConnection extends EventEmitter {
|
|
6
|
+
constructor(params, client) {
|
|
7
|
+
super();
|
|
8
|
+
this.isPolling = false;
|
|
9
|
+
this.isClosed = false;
|
|
10
|
+
this.id = params.id;
|
|
11
|
+
this.topic = params.topic;
|
|
12
|
+
this.role = params.role;
|
|
13
|
+
this.pc = params.pc;
|
|
14
|
+
this.localPeerId = params.localPeerId;
|
|
15
|
+
this.remotePeerId = params.remotePeerId;
|
|
16
|
+
this.client = client;
|
|
17
|
+
this.dataChannels = new Map();
|
|
18
|
+
this.pollingIntervalMs = params.pollingInterval;
|
|
19
|
+
this.connectionTimeoutMs = params.connectionTimeout;
|
|
20
|
+
this.setupEventHandlers();
|
|
21
|
+
this.startConnectionTimeout();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Setup RTCPeerConnection event handlers
|
|
25
|
+
*/
|
|
26
|
+
setupEventHandlers() {
|
|
27
|
+
// ICE candidate gathering
|
|
28
|
+
this.pc.onicecandidate = (event) => {
|
|
29
|
+
if (event.candidate && !this.isClosed) {
|
|
30
|
+
this.sendIceCandidate(event.candidate).catch((err) => {
|
|
31
|
+
this.emit('error', new Error(`Failed to send ICE candidate: ${err.message}`));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
// Connection state changes
|
|
36
|
+
this.pc.onconnectionstatechange = () => {
|
|
37
|
+
this.handleConnectionStateChange();
|
|
38
|
+
};
|
|
39
|
+
// Remote data channels
|
|
40
|
+
this.pc.ondatachannel = (event) => {
|
|
41
|
+
this.handleRemoteDataChannel(event.channel);
|
|
42
|
+
};
|
|
43
|
+
// Remote media streams
|
|
44
|
+
this.pc.ontrack = (event) => {
|
|
45
|
+
if (event.streams && event.streams[0]) {
|
|
46
|
+
this.emit('stream', event.streams[0]);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
// ICE connection state changes
|
|
50
|
+
this.pc.oniceconnectionstatechange = () => {
|
|
51
|
+
const state = this.pc.iceConnectionState;
|
|
52
|
+
if (state === 'failed' || state === 'closed') {
|
|
53
|
+
this.emit('error', new Error(`ICE connection ${state}`));
|
|
54
|
+
if (state === 'failed') {
|
|
55
|
+
this.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Handle RTCPeerConnection state changes
|
|
62
|
+
*/
|
|
63
|
+
handleConnectionStateChange() {
|
|
64
|
+
const state = this.pc.connectionState;
|
|
65
|
+
switch (state) {
|
|
66
|
+
case 'connected':
|
|
67
|
+
this.clearConnectionTimeout();
|
|
68
|
+
this.stopPolling();
|
|
69
|
+
this.emit('connect');
|
|
70
|
+
break;
|
|
71
|
+
case 'disconnected':
|
|
72
|
+
this.emit('disconnect');
|
|
73
|
+
break;
|
|
74
|
+
case 'failed':
|
|
75
|
+
this.emit('error', new Error('Connection failed'));
|
|
76
|
+
this.close();
|
|
77
|
+
break;
|
|
78
|
+
case 'closed':
|
|
79
|
+
this.emit('disconnect');
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Send an ICE candidate to the remote peer via signaling server
|
|
85
|
+
*/
|
|
86
|
+
async sendIceCandidate(candidate) {
|
|
87
|
+
try {
|
|
88
|
+
await this.client.sendAnswer({
|
|
89
|
+
code: this.id,
|
|
90
|
+
candidate: JSON.stringify(candidate.toJSON()),
|
|
91
|
+
side: this.role,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
throw new Error(`Failed to send ICE candidate: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Start polling for remote session data (answer/candidates)
|
|
100
|
+
*/
|
|
101
|
+
startPolling() {
|
|
102
|
+
if (this.isPolling || this.isClosed) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.isPolling = true;
|
|
106
|
+
// Poll immediately
|
|
107
|
+
this.poll().catch((err) => {
|
|
108
|
+
this.emit('error', new Error(`Poll error: ${err.message}`));
|
|
109
|
+
});
|
|
110
|
+
// Set up interval polling
|
|
111
|
+
this.pollingInterval = setInterval(() => {
|
|
112
|
+
this.poll().catch((err) => {
|
|
113
|
+
this.emit('error', new Error(`Poll error: ${err.message}`));
|
|
114
|
+
});
|
|
115
|
+
}, this.pollingIntervalMs);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Stop polling
|
|
119
|
+
*/
|
|
120
|
+
stopPolling() {
|
|
121
|
+
this.isPolling = false;
|
|
122
|
+
if (this.pollingInterval) {
|
|
123
|
+
clearInterval(this.pollingInterval);
|
|
124
|
+
this.pollingInterval = undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Poll the signaling server for remote data
|
|
129
|
+
*/
|
|
130
|
+
async poll() {
|
|
131
|
+
if (this.isClosed) {
|
|
132
|
+
this.stopPolling();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const response = await this.client.poll(this.id, this.role);
|
|
137
|
+
if (this.role === 'offerer') {
|
|
138
|
+
const offererResponse = response;
|
|
139
|
+
// Apply answer if received and not yet applied
|
|
140
|
+
if (offererResponse.answer && !this.pc.currentRemoteDescription) {
|
|
141
|
+
await this.pc.setRemoteDescription({
|
|
142
|
+
type: 'answer',
|
|
143
|
+
sdp: offererResponse.answer,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// Apply ICE candidates
|
|
147
|
+
if (offererResponse.answerCandidates && offererResponse.answerCandidates.length > 0) {
|
|
148
|
+
for (const candidateStr of offererResponse.answerCandidates) {
|
|
149
|
+
try {
|
|
150
|
+
const candidate = JSON.parse(candidateStr);
|
|
151
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.warn('Failed to add ICE candidate:', err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Answerer role
|
|
161
|
+
const answererResponse = response;
|
|
162
|
+
// Apply ICE candidates from offerer
|
|
163
|
+
if (answererResponse.offerCandidates && answererResponse.offerCandidates.length > 0) {
|
|
164
|
+
for (const candidateStr of answererResponse.offerCandidates) {
|
|
165
|
+
try {
|
|
166
|
+
const candidate = JSON.parse(candidateStr);
|
|
167
|
+
await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
console.warn('Failed to add ICE candidate:', err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
// Session not found or expired
|
|
178
|
+
if (err.message.includes('404') || err.message.includes('not found')) {
|
|
179
|
+
this.emit('error', new Error('Session not found or expired'));
|
|
180
|
+
this.close();
|
|
181
|
+
}
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Handle remotely created data channel
|
|
187
|
+
*/
|
|
188
|
+
handleRemoteDataChannel(channel) {
|
|
189
|
+
this.dataChannels.set(channel.label, channel);
|
|
190
|
+
this.emit('datachannel', channel);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get or create a data channel
|
|
194
|
+
*/
|
|
195
|
+
dataChannel(label, options) {
|
|
196
|
+
let channel = this.dataChannels.get(label);
|
|
197
|
+
if (!channel) {
|
|
198
|
+
channel = this.pc.createDataChannel(label, options);
|
|
199
|
+
this.dataChannels.set(label, channel);
|
|
200
|
+
}
|
|
201
|
+
return channel;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Add a local media stream to the connection
|
|
205
|
+
*/
|
|
206
|
+
addStream(stream) {
|
|
207
|
+
stream.getTracks().forEach(track => {
|
|
208
|
+
this.pc.addTrack(track, stream);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get the underlying RTCPeerConnection for advanced usage
|
|
213
|
+
*/
|
|
214
|
+
getPeerConnection() {
|
|
215
|
+
return this.pc;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Start connection timeout
|
|
219
|
+
*/
|
|
220
|
+
startConnectionTimeout() {
|
|
221
|
+
this.connectionTimer = setTimeout(() => {
|
|
222
|
+
if (this.pc.connectionState !== 'connected') {
|
|
223
|
+
this.emit('error', new Error('Connection timeout'));
|
|
224
|
+
this.close();
|
|
225
|
+
}
|
|
226
|
+
}, this.connectionTimeoutMs);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Clear connection timeout
|
|
230
|
+
*/
|
|
231
|
+
clearConnectionTimeout() {
|
|
232
|
+
if (this.connectionTimer) {
|
|
233
|
+
clearTimeout(this.connectionTimer);
|
|
234
|
+
this.connectionTimer = undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Close the connection and cleanup resources
|
|
239
|
+
*/
|
|
240
|
+
close() {
|
|
241
|
+
if (this.isClosed) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.isClosed = true;
|
|
245
|
+
this.stopPolling();
|
|
246
|
+
this.clearConnectionTimeout();
|
|
247
|
+
// Close all data channels
|
|
248
|
+
this.dataChannels.forEach(dc => {
|
|
249
|
+
if (dc.readyState === 'open' || dc.readyState === 'connecting') {
|
|
250
|
+
dc.close();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
this.dataChannels.clear();
|
|
254
|
+
// Close peer connection
|
|
255
|
+
if (this.pc.connectionState !== 'closed') {
|
|
256
|
+
this.pc.close();
|
|
257
|
+
}
|
|
258
|
+
this.emit('disconnect');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple EventEmitter implementation for browser and Node.js compatibility
|
|
3
|
+
*/
|
|
4
|
+
export declare class EventEmitter {
|
|
5
|
+
private events;
|
|
6
|
+
constructor();
|
|
7
|
+
/**
|
|
8
|
+
* Register an event listener
|
|
9
|
+
*/
|
|
10
|
+
on(event: string, listener: Function): this;
|
|
11
|
+
/**
|
|
12
|
+
* Register a one-time event listener
|
|
13
|
+
*/
|
|
14
|
+
once(event: string, listener: Function): this;
|
|
15
|
+
/**
|
|
16
|
+
* Remove an event listener
|
|
17
|
+
*/
|
|
18
|
+
off(event: string, listener: Function): this;
|
|
19
|
+
/**
|
|
20
|
+
* Emit an event
|
|
21
|
+
*/
|
|
22
|
+
emit(event: string, ...args: any[]): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Remove all listeners for an event (or all events if not specified)
|
|
25
|
+
*/
|
|
26
|
+
removeAllListeners(event?: string): this;
|
|
27
|
+
/**
|
|
28
|
+
* Get listener count for an event
|
|
29
|
+
*/
|
|
30
|
+
listenerCount(event: string): number;
|
|
31
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple EventEmitter implementation for browser and Node.js compatibility
|
|
3
|
+
*/
|
|
4
|
+
export class EventEmitter {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.events = new Map();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Register an event listener
|
|
10
|
+
*/
|
|
11
|
+
on(event, listener) {
|
|
12
|
+
if (!this.events.has(event)) {
|
|
13
|
+
this.events.set(event, new Set());
|
|
14
|
+
}
|
|
15
|
+
this.events.get(event).add(listener);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Register a one-time event listener
|
|
20
|
+
*/
|
|
21
|
+
once(event, listener) {
|
|
22
|
+
const onceWrapper = (...args) => {
|
|
23
|
+
this.off(event, onceWrapper);
|
|
24
|
+
listener.apply(this, args);
|
|
25
|
+
};
|
|
26
|
+
return this.on(event, onceWrapper);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Remove an event listener
|
|
30
|
+
*/
|
|
31
|
+
off(event, listener) {
|
|
32
|
+
const listeners = this.events.get(event);
|
|
33
|
+
if (listeners) {
|
|
34
|
+
listeners.delete(listener);
|
|
35
|
+
if (listeners.size === 0) {
|
|
36
|
+
this.events.delete(event);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Emit an event
|
|
43
|
+
*/
|
|
44
|
+
emit(event, ...args) {
|
|
45
|
+
const listeners = this.events.get(event);
|
|
46
|
+
if (!listeners || listeners.size === 0) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
listeners.forEach(listener => {
|
|
50
|
+
try {
|
|
51
|
+
listener.apply(this, args);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error(`Error in ${event} event listener:`, err);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Remove all listeners for an event (or all events if not specified)
|
|
61
|
+
*/
|
|
62
|
+
removeAllListeners(event) {
|
|
63
|
+
if (event) {
|
|
64
|
+
this.events.delete(event);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.events.clear();
|
|
68
|
+
}
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get listener count for an event
|
|
73
|
+
*/
|
|
74
|
+
listenerCount(event) {
|
|
75
|
+
const listeners = this.events.get(event);
|
|
76
|
+
return listeners ? listeners.size : 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,30 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```typescript
|
|
6
|
-
* import { RondevuClient } from '@xtr-dev/rondevu-client';
|
|
7
|
-
*
|
|
8
|
-
* const client = new RondevuClient({
|
|
9
|
-
* baseUrl: 'https://rondevu.example.com'
|
|
10
|
-
* });
|
|
11
|
-
*
|
|
12
|
-
* // Create an offer
|
|
13
|
-
* const { code } = await client.createOffer('my-room', {
|
|
14
|
-
* info: 'peer-123',
|
|
15
|
-
* offer: signalingData
|
|
16
|
-
* });
|
|
17
|
-
*
|
|
18
|
-
* // Discover peers
|
|
19
|
-
* const { sessions } = await client.listSessions('my-room');
|
|
20
|
-
*
|
|
21
|
-
* // Send answer
|
|
22
|
-
* await client.sendAnswer({
|
|
23
|
-
* code: sessions[0].code,
|
|
24
|
-
* answer: answerData,
|
|
25
|
-
* side: 'answerer'
|
|
26
|
-
* });
|
|
27
|
-
* ```
|
|
2
|
+
* @xtr-dev/rondevu-client
|
|
3
|
+
* WebRTC peer signaling and discovery client
|
|
28
4
|
*/
|
|
5
|
+
export { Rondevu } from './rondevu';
|
|
6
|
+
export { RondevuConnection } from './connection';
|
|
29
7
|
export { RondevuClient } from './client';
|
|
30
|
-
export
|
|
8
|
+
export type { RondevuOptions, JoinOptions, ConnectionRole, RondevuConnectionParams, RondevuConnectionEvents, Side, Session, TopicInfo, Pagination, ListTopicsResponse, ListSessionsResponse, CreateOfferRequest, CreateOfferResponse, AnswerRequest, AnswerResponse, PollRequest, PollOffererResponse, PollAnswererResponse, PollResponse, HealthResponse, ErrorResponse, RondevuClientOptions, } from './types';
|
package/dist/index.js
CHANGED
|
@@ -1,48 +1,10 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* ```typescript
|
|
7
|
-
* import { RondevuClient } from '@xtr-dev/rondevu-client';
|
|
8
|
-
*
|
|
9
|
-
* const client = new RondevuClient({
|
|
10
|
-
* baseUrl: 'https://rondevu.example.com'
|
|
11
|
-
* });
|
|
12
|
-
*
|
|
13
|
-
* // Create an offer
|
|
14
|
-
* const { code } = await client.createOffer('my-room', {
|
|
15
|
-
* info: 'peer-123',
|
|
16
|
-
* offer: signalingData
|
|
17
|
-
* });
|
|
18
|
-
*
|
|
19
|
-
* // Discover peers
|
|
20
|
-
* const { sessions } = await client.listSessions('my-room');
|
|
21
|
-
*
|
|
22
|
-
* // Send answer
|
|
23
|
-
* await client.sendAnswer({
|
|
24
|
-
* code: sessions[0].code,
|
|
25
|
-
* answer: answerData,
|
|
26
|
-
* side: 'answerer'
|
|
27
|
-
* });
|
|
28
|
-
* ```
|
|
2
|
+
* @xtr-dev/rondevu-client
|
|
3
|
+
* WebRTC peer signaling and discovery client
|
|
29
4
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Object.defineProperty(o, k2, desc);
|
|
37
|
-
}) : (function(o, m, k, k2) {
|
|
38
|
-
if (k2 === undefined) k2 = k;
|
|
39
|
-
o[k2] = m[k];
|
|
40
|
-
}));
|
|
41
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
42
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
43
|
-
};
|
|
44
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.RondevuClient = void 0;
|
|
46
|
-
var client_1 = require("./client");
|
|
47
|
-
Object.defineProperty(exports, "RondevuClient", { enumerable: true, get: function () { return client_1.RondevuClient; } });
|
|
48
|
-
__exportStar(require("./types"), exports);
|
|
5
|
+
// Export main WebRTC client class
|
|
6
|
+
export { Rondevu } from './rondevu';
|
|
7
|
+
// Export connection class
|
|
8
|
+
export { RondevuConnection } from './connection';
|
|
9
|
+
// Export low-level signaling client (for advanced usage)
|
|
10
|
+
export { RondevuClient } from './client';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { RondevuConnection } from './connection';
|
|
2
|
+
import { RondevuOptions, JoinOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Main Rondevu WebRTC client with automatic connection management
|
|
5
|
+
*/
|
|
6
|
+
export declare class Rondevu {
|
|
7
|
+
readonly peerId: string;
|
|
8
|
+
private client;
|
|
9
|
+
private rtcConfig?;
|
|
10
|
+
private pollingInterval;
|
|
11
|
+
private connectionTimeout;
|
|
12
|
+
/**
|
|
13
|
+
* Creates a new Rondevu client instance
|
|
14
|
+
* @param options - Client configuration options
|
|
15
|
+
*/
|
|
16
|
+
constructor(options: RondevuOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Generate a unique peer ID
|
|
19
|
+
*/
|
|
20
|
+
private generatePeerId;
|
|
21
|
+
/**
|
|
22
|
+
* Update the peer ID (useful when user identity changes)
|
|
23
|
+
*/
|
|
24
|
+
updatePeerId(newPeerId: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Create a new connection (offerer role)
|
|
27
|
+
* @param id - Connection identifier
|
|
28
|
+
* @param topic - Topic name for grouping connections
|
|
29
|
+
* @returns Promise that resolves to RondevuConnection
|
|
30
|
+
*/
|
|
31
|
+
create(id: string, topic: string): Promise<RondevuConnection>;
|
|
32
|
+
/**
|
|
33
|
+
* Connect to an existing connection by ID (answerer role)
|
|
34
|
+
* @param id - Connection identifier
|
|
35
|
+
* @returns Promise that resolves to RondevuConnection
|
|
36
|
+
*/
|
|
37
|
+
connect(id: string): Promise<RondevuConnection>;
|
|
38
|
+
/**
|
|
39
|
+
* Join a topic and discover available peers (answerer role)
|
|
40
|
+
* @param topic - Topic name
|
|
41
|
+
* @param options - Optional join options for filtering and selection
|
|
42
|
+
* @returns Promise that resolves to RondevuConnection
|
|
43
|
+
*/
|
|
44
|
+
join(topic: string, options?: JoinOptions): Promise<RondevuConnection>;
|
|
45
|
+
/**
|
|
46
|
+
* Select a session based on strategy
|
|
47
|
+
*/
|
|
48
|
+
private selectSession;
|
|
49
|
+
/**
|
|
50
|
+
* Wait for ICE gathering to complete
|
|
51
|
+
*/
|
|
52
|
+
private waitForIceGathering;
|
|
53
|
+
/**
|
|
54
|
+
* Find a session by connection ID
|
|
55
|
+
* This requires polling since we don't know which topic it's in
|
|
56
|
+
*/
|
|
57
|
+
private findSessionById;
|
|
58
|
+
}
|
package/dist/rondevu.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { RondevuClient } from './client';
|
|
2
|
+
import { RondevuConnection } from './connection';
|
|
3
|
+
/**
|
|
4
|
+
* Main Rondevu WebRTC client with automatic connection management
|
|
5
|
+
*/
|
|
6
|
+
export class Rondevu {
|
|
7
|
+
/**
|
|
8
|
+
* Creates a new Rondevu client instance
|
|
9
|
+
* @param options - Client configuration options
|
|
10
|
+
*/
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.client = new RondevuClient({
|
|
13
|
+
baseUrl: options.baseUrl,
|
|
14
|
+
origin: options.origin,
|
|
15
|
+
fetch: options.fetch,
|
|
16
|
+
});
|
|
17
|
+
// Auto-generate peer ID if not provided
|
|
18
|
+
this.peerId = options.peerId || this.generatePeerId();
|
|
19
|
+
this.rtcConfig = options.rtcConfig;
|
|
20
|
+
this.pollingInterval = options.pollingInterval || 1000;
|
|
21
|
+
this.connectionTimeout = options.connectionTimeout || 30000;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate a unique peer ID
|
|
25
|
+
*/
|
|
26
|
+
generatePeerId() {
|
|
27
|
+
return `rdv_${Math.random().toString(36).substring(2, 14)}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Update the peer ID (useful when user identity changes)
|
|
31
|
+
*/
|
|
32
|
+
updatePeerId(newPeerId) {
|
|
33
|
+
this.peerId = newPeerId;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a new connection (offerer role)
|
|
37
|
+
* @param id - Connection identifier
|
|
38
|
+
* @param topic - Topic name for grouping connections
|
|
39
|
+
* @returns Promise that resolves to RondevuConnection
|
|
40
|
+
*/
|
|
41
|
+
async create(id, topic) {
|
|
42
|
+
// Create peer connection
|
|
43
|
+
const pc = new RTCPeerConnection(this.rtcConfig);
|
|
44
|
+
// Create initial data channel for negotiation (required for offer creation)
|
|
45
|
+
pc.createDataChannel('_negotiation');
|
|
46
|
+
// Generate offer
|
|
47
|
+
const offer = await pc.createOffer();
|
|
48
|
+
await pc.setLocalDescription(offer);
|
|
49
|
+
// Wait for ICE gathering to complete
|
|
50
|
+
await this.waitForIceGathering(pc);
|
|
51
|
+
// Create session on server with custom code
|
|
52
|
+
await this.client.createOffer(topic, {
|
|
53
|
+
peerId: this.peerId,
|
|
54
|
+
offer: pc.localDescription.sdp,
|
|
55
|
+
code: id,
|
|
56
|
+
});
|
|
57
|
+
// Create connection object
|
|
58
|
+
const connectionParams = {
|
|
59
|
+
id,
|
|
60
|
+
topic,
|
|
61
|
+
role: 'offerer',
|
|
62
|
+
pc,
|
|
63
|
+
localPeerId: this.peerId,
|
|
64
|
+
remotePeerId: '', // Will be populated when answer is received
|
|
65
|
+
pollingInterval: this.pollingInterval,
|
|
66
|
+
connectionTimeout: this.connectionTimeout,
|
|
67
|
+
};
|
|
68
|
+
const connection = new RondevuConnection(connectionParams, this.client);
|
|
69
|
+
// Start polling for answer
|
|
70
|
+
connection.startPolling();
|
|
71
|
+
return connection;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Connect to an existing connection by ID (answerer role)
|
|
75
|
+
* @param id - Connection identifier
|
|
76
|
+
* @returns Promise that resolves to RondevuConnection
|
|
77
|
+
*/
|
|
78
|
+
async connect(id) {
|
|
79
|
+
// Poll server to get session by ID
|
|
80
|
+
const sessionData = await this.findSessionById(id);
|
|
81
|
+
if (!sessionData) {
|
|
82
|
+
throw new Error(`Connection ${id} not found or expired`);
|
|
83
|
+
}
|
|
84
|
+
// Create peer connection
|
|
85
|
+
const pc = new RTCPeerConnection(this.rtcConfig);
|
|
86
|
+
// Set remote offer
|
|
87
|
+
await pc.setRemoteDescription({
|
|
88
|
+
type: 'offer',
|
|
89
|
+
sdp: sessionData.offer,
|
|
90
|
+
});
|
|
91
|
+
// Generate answer
|
|
92
|
+
const answer = await pc.createAnswer();
|
|
93
|
+
await pc.setLocalDescription(answer);
|
|
94
|
+
// Wait for ICE gathering
|
|
95
|
+
await this.waitForIceGathering(pc);
|
|
96
|
+
// Send answer to server
|
|
97
|
+
await this.client.sendAnswer({
|
|
98
|
+
code: id,
|
|
99
|
+
answer: pc.localDescription.sdp,
|
|
100
|
+
side: 'answerer',
|
|
101
|
+
});
|
|
102
|
+
// Create connection object
|
|
103
|
+
const connectionParams = {
|
|
104
|
+
id,
|
|
105
|
+
topic: sessionData.topic || 'unknown',
|
|
106
|
+
role: 'answerer',
|
|
107
|
+
pc,
|
|
108
|
+
localPeerId: this.peerId,
|
|
109
|
+
remotePeerId: sessionData.peerId,
|
|
110
|
+
pollingInterval: this.pollingInterval,
|
|
111
|
+
connectionTimeout: this.connectionTimeout,
|
|
112
|
+
};
|
|
113
|
+
const connection = new RondevuConnection(connectionParams, this.client);
|
|
114
|
+
// Start polling for ICE candidates
|
|
115
|
+
connection.startPolling();
|
|
116
|
+
return connection;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Join a topic and discover available peers (answerer role)
|
|
120
|
+
* @param topic - Topic name
|
|
121
|
+
* @param options - Optional join options for filtering and selection
|
|
122
|
+
* @returns Promise that resolves to RondevuConnection
|
|
123
|
+
*/
|
|
124
|
+
async join(topic, options) {
|
|
125
|
+
// List sessions in topic
|
|
126
|
+
const { sessions } = await this.client.listSessions(topic);
|
|
127
|
+
// Filter out self (sessions with our peer ID)
|
|
128
|
+
let availableSessions = sessions.filter(session => session.peerId !== this.peerId);
|
|
129
|
+
// Apply custom filter if provided
|
|
130
|
+
if (options?.filter) {
|
|
131
|
+
availableSessions = availableSessions.filter(options.filter);
|
|
132
|
+
}
|
|
133
|
+
if (availableSessions.length === 0) {
|
|
134
|
+
throw new Error(`No available peers in topic: ${topic}`);
|
|
135
|
+
}
|
|
136
|
+
// Select session based on strategy
|
|
137
|
+
const selectedSession = this.selectSession(availableSessions, options?.select || 'first');
|
|
138
|
+
// Connect to selected session
|
|
139
|
+
return this.connect(selectedSession.code);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Select a session based on strategy
|
|
143
|
+
*/
|
|
144
|
+
selectSession(sessions, strategy) {
|
|
145
|
+
switch (strategy) {
|
|
146
|
+
case 'first':
|
|
147
|
+
return sessions[0];
|
|
148
|
+
case 'newest':
|
|
149
|
+
return sessions.reduce((newest, session) => session.createdAt > newest.createdAt ? session : newest);
|
|
150
|
+
case 'oldest':
|
|
151
|
+
return sessions.reduce((oldest, session) => session.createdAt < oldest.createdAt ? session : oldest);
|
|
152
|
+
case 'random':
|
|
153
|
+
return sessions[Math.floor(Math.random() * sessions.length)];
|
|
154
|
+
default:
|
|
155
|
+
return sessions[0];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Wait for ICE gathering to complete
|
|
160
|
+
*/
|
|
161
|
+
async waitForIceGathering(pc) {
|
|
162
|
+
if (pc.iceGatheringState === 'complete') {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
const checkState = () => {
|
|
167
|
+
if (pc.iceGatheringState === 'complete') {
|
|
168
|
+
pc.removeEventListener('icegatheringstatechange', checkState);
|
|
169
|
+
resolve();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
pc.addEventListener('icegatheringstatechange', checkState);
|
|
173
|
+
// Also set a timeout in case gathering takes too long
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
pc.removeEventListener('icegatheringstatechange', checkState);
|
|
176
|
+
resolve();
|
|
177
|
+
}, 5000);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Find a session by connection ID
|
|
182
|
+
* This requires polling since we don't know which topic it's in
|
|
183
|
+
*/
|
|
184
|
+
async findSessionById(id) {
|
|
185
|
+
try {
|
|
186
|
+
// Try to poll for the session directly
|
|
187
|
+
// The poll endpoint should return the session data
|
|
188
|
+
const response = await this.client.poll(id, 'answerer');
|
|
189
|
+
const answererResponse = response;
|
|
190
|
+
if (answererResponse.offer) {
|
|
191
|
+
return {
|
|
192
|
+
code: id,
|
|
193
|
+
peerId: '', // Will be populated from session data
|
|
194
|
+
offer: answererResponse.offer,
|
|
195
|
+
topic: undefined,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
throw new Error(`Failed to find session ${id}: ${err.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface Session {
|
|
|
9
9
|
/** Unique session identifier (UUID) */
|
|
10
10
|
code: string;
|
|
11
11
|
/** Peer identifier/metadata */
|
|
12
|
-
|
|
12
|
+
peerId: string;
|
|
13
13
|
/** Signaling data for peer connection */
|
|
14
14
|
offer: string;
|
|
15
15
|
/** Additional signaling data from offerer */
|
|
@@ -59,9 +59,11 @@ export interface ListSessionsResponse {
|
|
|
59
59
|
*/
|
|
60
60
|
export interface CreateOfferRequest {
|
|
61
61
|
/** Peer identifier/metadata (max 1024 characters) */
|
|
62
|
-
|
|
62
|
+
peerId: string;
|
|
63
63
|
/** Signaling data for peer connection */
|
|
64
64
|
offer: string;
|
|
65
|
+
/** Optional custom connection code (if not provided, server generates UUID) */
|
|
66
|
+
code?: string;
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
67
69
|
* Response from POST /:topic/offer
|
|
@@ -144,3 +146,61 @@ export interface RondevuClientOptions {
|
|
|
144
146
|
/** Optional fetch implementation (for Node.js environments) */
|
|
145
147
|
fetch?: typeof fetch;
|
|
146
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Configuration options for Rondevu WebRTC client
|
|
151
|
+
*/
|
|
152
|
+
export interface RondevuOptions {
|
|
153
|
+
/** Base URL of the Rondevu server (e.g., 'https://example.com') */
|
|
154
|
+
baseUrl: string;
|
|
155
|
+
/** Peer identifier (optional, auto-generated if not provided) */
|
|
156
|
+
peerId?: string;
|
|
157
|
+
/** Origin header value for session isolation (defaults to baseUrl origin) */
|
|
158
|
+
origin?: string;
|
|
159
|
+
/** Optional fetch implementation (for Node.js environments) */
|
|
160
|
+
fetch?: typeof fetch;
|
|
161
|
+
/** WebRTC configuration (ICE servers, etc.) */
|
|
162
|
+
rtcConfig?: RTCConfiguration;
|
|
163
|
+
/** Polling interval in milliseconds (default: 1000) */
|
|
164
|
+
pollingInterval?: number;
|
|
165
|
+
/** Connection timeout in milliseconds (default: 30000) */
|
|
166
|
+
connectionTimeout?: number;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Options for joining a topic
|
|
170
|
+
*/
|
|
171
|
+
export interface JoinOptions {
|
|
172
|
+
/** Filter function to select specific sessions */
|
|
173
|
+
filter?: (session: {
|
|
174
|
+
code: string;
|
|
175
|
+
peerId: string;
|
|
176
|
+
}) => boolean;
|
|
177
|
+
/** Selection strategy for choosing a session */
|
|
178
|
+
select?: 'first' | 'newest' | 'oldest' | 'random';
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Connection role - whether this peer is creating or answering
|
|
182
|
+
*/
|
|
183
|
+
export type ConnectionRole = 'offerer' | 'answerer';
|
|
184
|
+
/**
|
|
185
|
+
* Parameters for creating a RondevuConnection
|
|
186
|
+
*/
|
|
187
|
+
export interface RondevuConnectionParams {
|
|
188
|
+
id: string;
|
|
189
|
+
topic: string;
|
|
190
|
+
role: ConnectionRole;
|
|
191
|
+
pc: RTCPeerConnection;
|
|
192
|
+
localPeerId: string;
|
|
193
|
+
remotePeerId: string;
|
|
194
|
+
pollingInterval: number;
|
|
195
|
+
connectionTimeout: number;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Event map for RondevuConnection events
|
|
199
|
+
*/
|
|
200
|
+
export interface RondevuConnectionEvents {
|
|
201
|
+
connect: () => void;
|
|
202
|
+
disconnect: () => void;
|
|
203
|
+
error: (error: Error) => void;
|
|
204
|
+
datachannel: (channel: RTCDataChannel) => void;
|
|
205
|
+
stream: (stream: MediaStream) => void;
|
|
206
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Signaling Types
|
|
3
|
+
// ============================================================================
|
|
4
|
+
export {};
|
package/package.json
CHANGED