@xtr-dev/rondevu-client 0.8.3 → 0.9.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/README.md +402 -436
- package/dist/durable/channel.d.ts +115 -0
- package/dist/durable/channel.js +301 -0
- package/dist/durable/connection.d.ts +125 -0
- package/dist/durable/connection.js +370 -0
- package/dist/durable/reconnection.d.ts +90 -0
- package/dist/durable/reconnection.js +127 -0
- package/dist/durable/service.d.ts +103 -0
- package/dist/durable/service.js +264 -0
- package/dist/durable/types.d.ts +149 -0
- package/dist/durable/types.js +28 -0
- package/dist/index.d.ts +5 -10
- package/dist/index.js +5 -9
- package/dist/offer-pool.d.ts +15 -3
- package/dist/offer-pool.js +34 -8
- package/dist/peer/exchanging-ice-state.js +10 -2
- package/dist/peer/index.d.ts +1 -1
- package/dist/peer/index.js +25 -3
- package/dist/peer/state.js +9 -1
- package/dist/rondevu.d.ts +88 -13
- package/dist/rondevu.js +110 -27
- package/dist/service-pool.d.ts +11 -3
- package/dist/service-pool.js +193 -44
- package/package.json +2 -2
- package/dist/bloom.d.ts +0 -30
- package/dist/bloom.js +0 -73
- package/dist/client.d.ts +0 -126
- package/dist/client.js +0 -171
- package/dist/connection.d.ts +0 -127
- package/dist/connection.js +0 -295
- package/dist/discovery.d.ts +0 -93
- package/dist/discovery.js +0 -164
- package/dist/peer.d.ts +0 -111
- package/dist/peer.js +0 -392
- package/dist/services.d.ts +0 -79
- package/dist/services.js +0 -206
- package/dist/types.d.ts +0 -157
- package/dist/types.js +0 -4
package/dist/discovery.js
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import RondevuPeer from './peer/index.js';
|
|
2
|
-
import { RondevuOffers } from './offers.js';
|
|
3
|
-
/**
|
|
4
|
-
* Rondevu Discovery API
|
|
5
|
-
* Handles service discovery and connections
|
|
6
|
-
*/
|
|
7
|
-
export class RondevuDiscovery {
|
|
8
|
-
constructor(baseUrl, credentials) {
|
|
9
|
-
this.baseUrl = baseUrl;
|
|
10
|
-
this.credentials = credentials;
|
|
11
|
-
this.offersApi = new RondevuOffers(baseUrl, credentials);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Lists all services for a username
|
|
15
|
-
* Returns UUIDs only for private services, full details for public
|
|
16
|
-
*/
|
|
17
|
-
async listServices(username) {
|
|
18
|
-
const response = await fetch(`${this.baseUrl}/usernames/${username}/services`);
|
|
19
|
-
if (!response.ok) {
|
|
20
|
-
throw new Error('Failed to list services');
|
|
21
|
-
}
|
|
22
|
-
const data = await response.json();
|
|
23
|
-
return {
|
|
24
|
-
username: data.username,
|
|
25
|
-
services: data.services
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Queries a service by FQN
|
|
30
|
-
* Returns UUID if service exists and is allowed
|
|
31
|
-
*/
|
|
32
|
-
async queryService(username, serviceFqn) {
|
|
33
|
-
const response = await fetch(`${this.baseUrl}/index/${username}/query`, {
|
|
34
|
-
method: 'POST',
|
|
35
|
-
headers: { 'Content-Type': 'application/json' },
|
|
36
|
-
body: JSON.stringify({ serviceFqn })
|
|
37
|
-
});
|
|
38
|
-
if (!response.ok) {
|
|
39
|
-
const error = await response.json();
|
|
40
|
-
throw new Error(error.error || 'Service not found');
|
|
41
|
-
}
|
|
42
|
-
const data = await response.json();
|
|
43
|
-
return {
|
|
44
|
-
uuid: data.uuid,
|
|
45
|
-
allowed: data.allowed
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Gets service details by UUID
|
|
50
|
-
*/
|
|
51
|
-
async getServiceDetails(uuid) {
|
|
52
|
-
const response = await fetch(`${this.baseUrl}/services/${uuid}`);
|
|
53
|
-
if (!response.ok) {
|
|
54
|
-
const error = await response.json();
|
|
55
|
-
throw new Error(error.error || 'Service not found');
|
|
56
|
-
}
|
|
57
|
-
const data = await response.json();
|
|
58
|
-
return {
|
|
59
|
-
serviceId: data.serviceId,
|
|
60
|
-
username: data.username,
|
|
61
|
-
serviceFqn: data.serviceFqn,
|
|
62
|
-
offerId: data.offerId,
|
|
63
|
-
sdp: data.sdp,
|
|
64
|
-
isPublic: data.isPublic,
|
|
65
|
-
metadata: data.metadata,
|
|
66
|
-
createdAt: data.createdAt,
|
|
67
|
-
expiresAt: data.expiresAt
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Connects to a service by UUID
|
|
72
|
-
*/
|
|
73
|
-
async connectToService(uuid, options) {
|
|
74
|
-
// Get service details
|
|
75
|
-
const service = await this.getServiceDetails(uuid);
|
|
76
|
-
// Create peer with the offer
|
|
77
|
-
const peer = new RondevuPeer(this.offersApi, options?.rtcConfig || {
|
|
78
|
-
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
|
79
|
-
});
|
|
80
|
-
// Set up event handlers
|
|
81
|
-
if (options?.onConnected) {
|
|
82
|
-
peer.on('connected', options.onConnected);
|
|
83
|
-
}
|
|
84
|
-
if (options?.onData) {
|
|
85
|
-
peer.on('datachannel', (channel) => {
|
|
86
|
-
channel.onmessage = (e) => options.onData(e.data);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
// Answer the offer
|
|
90
|
-
await peer.answer(service.offerId, service.sdp, {
|
|
91
|
-
topics: [], // V2 doesn't use topics
|
|
92
|
-
rtcConfig: options?.rtcConfig
|
|
93
|
-
});
|
|
94
|
-
return peer;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Convenience method: Query and connect in one call
|
|
98
|
-
* Returns both peer and data channel
|
|
99
|
-
*/
|
|
100
|
-
async connect(username, serviceFqn, options) {
|
|
101
|
-
// Query service
|
|
102
|
-
const query = await this.queryService(username, serviceFqn);
|
|
103
|
-
if (!query.allowed) {
|
|
104
|
-
throw new Error('Service access denied');
|
|
105
|
-
}
|
|
106
|
-
// Get service details
|
|
107
|
-
const service = await this.getServiceDetails(query.uuid);
|
|
108
|
-
// Create peer
|
|
109
|
-
const peer = new RondevuPeer(this.offersApi, options?.rtcConfig || {
|
|
110
|
-
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
|
111
|
-
});
|
|
112
|
-
// Answer the offer
|
|
113
|
-
await peer.answer(service.offerId, service.sdp, {
|
|
114
|
-
topics: [], // V2 doesn't use topics
|
|
115
|
-
rtcConfig: options?.rtcConfig
|
|
116
|
-
});
|
|
117
|
-
// Wait for data channel
|
|
118
|
-
const channel = await new Promise((resolve, reject) => {
|
|
119
|
-
const timeout = setTimeout(() => {
|
|
120
|
-
reject(new Error('Timeout waiting for data channel'));
|
|
121
|
-
}, 30000);
|
|
122
|
-
peer.on('datachannel', (ch) => {
|
|
123
|
-
clearTimeout(timeout);
|
|
124
|
-
resolve(ch);
|
|
125
|
-
});
|
|
126
|
-
peer.on('failed', (error) => {
|
|
127
|
-
clearTimeout(timeout);
|
|
128
|
-
reject(error);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
return { peer, channel };
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Convenience method: Connect to service by UUID with channel
|
|
135
|
-
*/
|
|
136
|
-
async connectByUuid(uuid, options) {
|
|
137
|
-
// Get service details
|
|
138
|
-
const service = await this.getServiceDetails(uuid);
|
|
139
|
-
// Create peer
|
|
140
|
-
const peer = new RondevuPeer(this.offersApi, options?.rtcConfig || {
|
|
141
|
-
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
|
142
|
-
});
|
|
143
|
-
// Answer the offer
|
|
144
|
-
await peer.answer(service.offerId, service.sdp, {
|
|
145
|
-
topics: [], // V2 doesn't use topics
|
|
146
|
-
rtcConfig: options?.rtcConfig
|
|
147
|
-
});
|
|
148
|
-
// Wait for data channel
|
|
149
|
-
const channel = await new Promise((resolve, reject) => {
|
|
150
|
-
const timeout = setTimeout(() => {
|
|
151
|
-
reject(new Error('Timeout waiting for data channel'));
|
|
152
|
-
}, 30000);
|
|
153
|
-
peer.on('datachannel', (ch) => {
|
|
154
|
-
clearTimeout(timeout);
|
|
155
|
-
resolve(ch);
|
|
156
|
-
});
|
|
157
|
-
peer.on('failed', (error) => {
|
|
158
|
-
clearTimeout(timeout);
|
|
159
|
-
reject(error);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
return { peer, channel };
|
|
163
|
-
}
|
|
164
|
-
}
|
package/dist/peer.d.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { RondevuOffers } from './offers.js';
|
|
2
|
-
import { EventEmitter } from './event-emitter.js';
|
|
3
|
-
/**
|
|
4
|
-
* Timeout configurations for different connection phases
|
|
5
|
-
*/
|
|
6
|
-
export interface PeerTimeouts {
|
|
7
|
-
/** Timeout for ICE gathering (default: 10000ms) */
|
|
8
|
-
iceGathering?: number;
|
|
9
|
-
/** Timeout for waiting for answer (default: 30000ms) */
|
|
10
|
-
waitingForAnswer?: number;
|
|
11
|
-
/** Timeout for creating answer (default: 10000ms) */
|
|
12
|
-
creatingAnswer?: number;
|
|
13
|
-
/** Timeout for ICE connection (default: 30000ms) */
|
|
14
|
-
iceConnection?: number;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Options for creating a peer connection
|
|
18
|
-
*/
|
|
19
|
-
export interface PeerOptions {
|
|
20
|
-
/** RTCConfiguration for the peer connection */
|
|
21
|
-
rtcConfig?: RTCConfiguration;
|
|
22
|
-
/** Topics to advertise this connection under */
|
|
23
|
-
topics: string[];
|
|
24
|
-
/** How long the offer should live (milliseconds) */
|
|
25
|
-
ttl?: number;
|
|
26
|
-
/** Whether to create a data channel automatically (for offerer) */
|
|
27
|
-
createDataChannel?: boolean;
|
|
28
|
-
/** Label for the automatically created data channel */
|
|
29
|
-
dataChannelLabel?: string;
|
|
30
|
-
/** Timeout configurations */
|
|
31
|
-
timeouts?: PeerTimeouts;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Events emitted by RondevuPeer
|
|
35
|
-
*/
|
|
36
|
-
export interface PeerEvents extends Record<string, (...args: any[]) => void> {
|
|
37
|
-
'state': (state: string) => void;
|
|
38
|
-
'connected': () => void;
|
|
39
|
-
'disconnected': () => void;
|
|
40
|
-
'failed': (error: Error) => void;
|
|
41
|
-
'datachannel': (channel: RTCDataChannel) => void;
|
|
42
|
-
'track': (event: RTCTrackEvent) => void;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Base class for peer connection states
|
|
46
|
-
*/
|
|
47
|
-
declare abstract class PeerState {
|
|
48
|
-
protected peer: RondevuPeer;
|
|
49
|
-
constructor(peer: RondevuPeer);
|
|
50
|
-
abstract get name(): string;
|
|
51
|
-
createOffer(options: PeerOptions): Promise<string>;
|
|
52
|
-
answer(offerId: string, offerSdp: string, options: PeerOptions): Promise<void>;
|
|
53
|
-
handleAnswer(sdp: string): Promise<void>;
|
|
54
|
-
handleIceCandidate(candidate: any): Promise<void>;
|
|
55
|
-
cleanup(): void;
|
|
56
|
-
close(): void;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* High-level WebRTC peer connection manager with state-based lifecycle
|
|
60
|
-
* Handles offer/answer exchange, ICE candidates, timeouts, and error recovery
|
|
61
|
-
*/
|
|
62
|
-
export default class RondevuPeer extends EventEmitter<PeerEvents> {
|
|
63
|
-
pc: RTCPeerConnection;
|
|
64
|
-
offersApi: RondevuOffers;
|
|
65
|
-
offerId?: string;
|
|
66
|
-
role?: 'offerer' | 'answerer';
|
|
67
|
-
private _state;
|
|
68
|
-
/**
|
|
69
|
-
* Current connection state name
|
|
70
|
-
*/
|
|
71
|
-
get stateName(): string;
|
|
72
|
-
/**
|
|
73
|
-
* Current state object (internal use)
|
|
74
|
-
*/
|
|
75
|
-
get state(): PeerState;
|
|
76
|
-
/**
|
|
77
|
-
* RTCPeerConnection state
|
|
78
|
-
*/
|
|
79
|
-
get connectionState(): RTCPeerConnectionState;
|
|
80
|
-
constructor(offersApi: RondevuOffers, rtcConfig?: RTCConfiguration);
|
|
81
|
-
/**
|
|
82
|
-
* Set up peer connection event handlers
|
|
83
|
-
*/
|
|
84
|
-
private setupPeerConnection;
|
|
85
|
-
/**
|
|
86
|
-
* Set new state and emit state change event
|
|
87
|
-
*/
|
|
88
|
-
setState(newState: PeerState): void;
|
|
89
|
-
/**
|
|
90
|
-
* Emit event (exposed for PeerState classes)
|
|
91
|
-
* @internal
|
|
92
|
-
*/
|
|
93
|
-
emitEvent<K extends keyof PeerEvents>(event: K, ...args: Parameters<PeerEvents[K]>): void;
|
|
94
|
-
/**
|
|
95
|
-
* Create an offer and advertise on topics
|
|
96
|
-
*/
|
|
97
|
-
createOffer(options: PeerOptions): Promise<string>;
|
|
98
|
-
/**
|
|
99
|
-
* Answer an existing offer
|
|
100
|
-
*/
|
|
101
|
-
answer(offerId: string, offerSdp: string, options: PeerOptions): Promise<void>;
|
|
102
|
-
/**
|
|
103
|
-
* Add a media track to the connection
|
|
104
|
-
*/
|
|
105
|
-
addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender;
|
|
106
|
-
/**
|
|
107
|
-
* Close the connection and clean up
|
|
108
|
-
*/
|
|
109
|
-
close(): void;
|
|
110
|
-
}
|
|
111
|
-
export {};
|
package/dist/peer.js
DELETED
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from './event-emitter.js';
|
|
2
|
-
/**
|
|
3
|
-
* Base class for peer connection states
|
|
4
|
-
*/
|
|
5
|
-
class PeerState {
|
|
6
|
-
constructor(peer) {
|
|
7
|
-
this.peer = peer;
|
|
8
|
-
}
|
|
9
|
-
async createOffer(options) {
|
|
10
|
-
throw new Error(`Cannot create offer in ${this.name} state`);
|
|
11
|
-
}
|
|
12
|
-
async answer(offerId, offerSdp, options) {
|
|
13
|
-
throw new Error(`Cannot answer in ${this.name} state`);
|
|
14
|
-
}
|
|
15
|
-
async handleAnswer(sdp) {
|
|
16
|
-
throw new Error(`Cannot handle answer in ${this.name} state`);
|
|
17
|
-
}
|
|
18
|
-
async handleIceCandidate(candidate) {
|
|
19
|
-
// ICE candidates can arrive in multiple states, so default is to add them
|
|
20
|
-
if (this.peer.pc.remoteDescription) {
|
|
21
|
-
await this.peer.pc.addIceCandidate(new RTCIceCandidate(candidate));
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
cleanup() {
|
|
25
|
-
// Override in states that need cleanup
|
|
26
|
-
}
|
|
27
|
-
close() {
|
|
28
|
-
this.cleanup();
|
|
29
|
-
this.peer.setState(new ClosedState(this.peer));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Initial idle state
|
|
34
|
-
*/
|
|
35
|
-
class IdleState extends PeerState {
|
|
36
|
-
get name() { return 'idle'; }
|
|
37
|
-
async createOffer(options) {
|
|
38
|
-
this.peer.setState(new CreatingOfferState(this.peer, options));
|
|
39
|
-
return this.peer.state.createOffer(options);
|
|
40
|
-
}
|
|
41
|
-
async answer(offerId, offerSdp, options) {
|
|
42
|
-
this.peer.setState(new AnsweringState(this.peer));
|
|
43
|
-
return this.peer.state.answer(offerId, offerSdp, options);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Creating offer and sending to server
|
|
48
|
-
*/
|
|
49
|
-
class CreatingOfferState extends PeerState {
|
|
50
|
-
constructor(peer, options) {
|
|
51
|
-
super(peer);
|
|
52
|
-
this.options = options;
|
|
53
|
-
}
|
|
54
|
-
get name() { return 'creating-offer'; }
|
|
55
|
-
async createOffer(options) {
|
|
56
|
-
try {
|
|
57
|
-
this.peer.role = 'offerer';
|
|
58
|
-
// Create data channel if requested
|
|
59
|
-
if (options.createDataChannel !== false) {
|
|
60
|
-
const channel = this.peer.pc.createDataChannel(options.dataChannelLabel || 'data');
|
|
61
|
-
this.peer.emitEvent('datachannel', channel);
|
|
62
|
-
}
|
|
63
|
-
// Create WebRTC offer
|
|
64
|
-
const offer = await this.peer.pc.createOffer();
|
|
65
|
-
await this.peer.pc.setLocalDescription(offer);
|
|
66
|
-
// Send offer to server immediately (don't wait for ICE)
|
|
67
|
-
const offers = await this.peer.offersApi.create([{
|
|
68
|
-
sdp: offer.sdp,
|
|
69
|
-
topics: options.topics,
|
|
70
|
-
ttl: options.ttl || 300000
|
|
71
|
-
}]);
|
|
72
|
-
const offerId = offers[0].id;
|
|
73
|
-
this.peer.offerId = offerId;
|
|
74
|
-
// Enable trickle ICE - send candidates as they arrive
|
|
75
|
-
this.peer.pc.onicecandidate = async (event) => {
|
|
76
|
-
if (event.candidate && offerId) {
|
|
77
|
-
const candidateData = event.candidate.toJSON();
|
|
78
|
-
if (candidateData.candidate && candidateData.candidate !== '') {
|
|
79
|
-
try {
|
|
80
|
-
await this.peer.offersApi.addIceCandidates(offerId, [candidateData]);
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
console.error('Error sending ICE candidate:', err);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
// Transition to waiting for answer
|
|
89
|
-
this.peer.setState(new WaitingForAnswerState(this.peer, offerId, options));
|
|
90
|
-
return offerId;
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
this.peer.setState(new FailedState(this.peer, error));
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Waiting for answer from another peer
|
|
100
|
-
*/
|
|
101
|
-
class WaitingForAnswerState extends PeerState {
|
|
102
|
-
constructor(peer, offerId, options) {
|
|
103
|
-
super(peer);
|
|
104
|
-
this.offerId = offerId;
|
|
105
|
-
this.options = options;
|
|
106
|
-
this.startPolling();
|
|
107
|
-
}
|
|
108
|
-
get name() { return 'waiting-for-answer'; }
|
|
109
|
-
startPolling() {
|
|
110
|
-
const answerTimeout = this.options.timeouts?.waitingForAnswer || 30000;
|
|
111
|
-
this.timeout = setTimeout(() => {
|
|
112
|
-
this.cleanup();
|
|
113
|
-
this.peer.setState(new FailedState(this.peer, new Error('Timeout waiting for answer')));
|
|
114
|
-
}, answerTimeout);
|
|
115
|
-
this.pollingInterval = setInterval(async () => {
|
|
116
|
-
try {
|
|
117
|
-
const answers = await this.peer.offersApi.getAnswers();
|
|
118
|
-
const myAnswer = answers.find(a => a.offerId === this.offerId);
|
|
119
|
-
if (myAnswer) {
|
|
120
|
-
this.cleanup();
|
|
121
|
-
await this.handleAnswer(myAnswer.sdp);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch (err) {
|
|
125
|
-
console.error('Error polling for answers:', err);
|
|
126
|
-
if (err instanceof Error && err.message.includes('not found')) {
|
|
127
|
-
this.cleanup();
|
|
128
|
-
this.peer.setState(new FailedState(this.peer, new Error('Offer expired or not found')));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}, 2000);
|
|
132
|
-
}
|
|
133
|
-
async handleAnswer(sdp) {
|
|
134
|
-
try {
|
|
135
|
-
await this.peer.pc.setRemoteDescription({
|
|
136
|
-
type: 'answer',
|
|
137
|
-
sdp
|
|
138
|
-
});
|
|
139
|
-
// Transition to exchanging ICE
|
|
140
|
-
this.peer.setState(new ExchangingIceState(this.peer, this.offerId, this.options));
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
this.peer.setState(new FailedState(this.peer, error));
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
cleanup() {
|
|
147
|
-
if (this.pollingInterval)
|
|
148
|
-
clearInterval(this.pollingInterval);
|
|
149
|
-
if (this.timeout)
|
|
150
|
-
clearTimeout(this.timeout);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Answering an offer and sending to server
|
|
155
|
-
*/
|
|
156
|
-
class AnsweringState extends PeerState {
|
|
157
|
-
constructor(peer) {
|
|
158
|
-
super(peer);
|
|
159
|
-
}
|
|
160
|
-
get name() { return 'answering'; }
|
|
161
|
-
async answer(offerId, offerSdp, options) {
|
|
162
|
-
try {
|
|
163
|
-
this.peer.role = 'answerer';
|
|
164
|
-
this.peer.offerId = offerId;
|
|
165
|
-
// Set remote description
|
|
166
|
-
await this.peer.pc.setRemoteDescription({
|
|
167
|
-
type: 'offer',
|
|
168
|
-
sdp: offerSdp
|
|
169
|
-
});
|
|
170
|
-
// Create answer
|
|
171
|
-
const answer = await this.peer.pc.createAnswer();
|
|
172
|
-
await this.peer.pc.setLocalDescription(answer);
|
|
173
|
-
// Send answer to server immediately (don't wait for ICE)
|
|
174
|
-
await this.peer.offersApi.answer(offerId, answer.sdp);
|
|
175
|
-
// Enable trickle ICE - send candidates as they arrive
|
|
176
|
-
this.peer.pc.onicecandidate = async (event) => {
|
|
177
|
-
if (event.candidate && offerId) {
|
|
178
|
-
const candidateData = event.candidate.toJSON();
|
|
179
|
-
if (candidateData.candidate && candidateData.candidate !== '') {
|
|
180
|
-
try {
|
|
181
|
-
await this.peer.offersApi.addIceCandidates(offerId, [candidateData]);
|
|
182
|
-
}
|
|
183
|
-
catch (err) {
|
|
184
|
-
console.error('Error sending ICE candidate:', err);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
// Transition to exchanging ICE
|
|
190
|
-
this.peer.setState(new ExchangingIceState(this.peer, offerId, options));
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
this.peer.setState(new FailedState(this.peer, error));
|
|
194
|
-
throw error;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Exchanging ICE candidates and waiting for connection
|
|
200
|
-
*/
|
|
201
|
-
class ExchangingIceState extends PeerState {
|
|
202
|
-
constructor(peer, offerId, options) {
|
|
203
|
-
super(peer);
|
|
204
|
-
this.offerId = offerId;
|
|
205
|
-
this.options = options;
|
|
206
|
-
this.lastIceTimestamp = 0;
|
|
207
|
-
this.startPolling();
|
|
208
|
-
}
|
|
209
|
-
get name() { return 'exchanging-ice'; }
|
|
210
|
-
startPolling() {
|
|
211
|
-
const connectionTimeout = this.options.timeouts?.iceConnection || 30000;
|
|
212
|
-
this.timeout = setTimeout(() => {
|
|
213
|
-
this.cleanup();
|
|
214
|
-
this.peer.setState(new FailedState(this.peer, new Error('ICE connection timeout')));
|
|
215
|
-
}, connectionTimeout);
|
|
216
|
-
this.pollingInterval = setInterval(async () => {
|
|
217
|
-
try {
|
|
218
|
-
const candidates = await this.peer.offersApi.getIceCandidates(this.offerId, this.lastIceTimestamp);
|
|
219
|
-
for (const cand of candidates) {
|
|
220
|
-
if (cand.candidate && cand.candidate.candidate && cand.candidate.candidate !== '') {
|
|
221
|
-
try {
|
|
222
|
-
await this.peer.pc.addIceCandidate(new RTCIceCandidate(cand.candidate));
|
|
223
|
-
this.lastIceTimestamp = cand.createdAt;
|
|
224
|
-
}
|
|
225
|
-
catch (err) {
|
|
226
|
-
console.warn('Failed to add ICE candidate:', err);
|
|
227
|
-
this.lastIceTimestamp = cand.createdAt;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
this.lastIceTimestamp = cand.createdAt;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
console.error('Error polling for ICE candidates:', err);
|
|
237
|
-
if (err instanceof Error && err.message.includes('not found')) {
|
|
238
|
-
this.cleanup();
|
|
239
|
-
this.peer.setState(new FailedState(this.peer, new Error('Offer expired or not found')));
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}, 1000);
|
|
243
|
-
}
|
|
244
|
-
cleanup() {
|
|
245
|
-
if (this.pollingInterval)
|
|
246
|
-
clearInterval(this.pollingInterval);
|
|
247
|
-
if (this.timeout)
|
|
248
|
-
clearTimeout(this.timeout);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Successfully connected state
|
|
253
|
-
*/
|
|
254
|
-
class ConnectedState extends PeerState {
|
|
255
|
-
get name() { return 'connected'; }
|
|
256
|
-
cleanup() {
|
|
257
|
-
// Keep connection alive, but stop any polling
|
|
258
|
-
// The peer connection will handle disconnects via onconnectionstatechange
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Failed state
|
|
263
|
-
*/
|
|
264
|
-
class FailedState extends PeerState {
|
|
265
|
-
constructor(peer, error) {
|
|
266
|
-
super(peer);
|
|
267
|
-
this.error = error;
|
|
268
|
-
peer.emitEvent('failed', error);
|
|
269
|
-
}
|
|
270
|
-
get name() { return 'failed'; }
|
|
271
|
-
cleanup() {
|
|
272
|
-
// Connection is failed, clean up resources
|
|
273
|
-
this.peer.pc.close();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Closed state
|
|
278
|
-
*/
|
|
279
|
-
class ClosedState extends PeerState {
|
|
280
|
-
get name() { return 'closed'; }
|
|
281
|
-
cleanup() {
|
|
282
|
-
this.peer.pc.close();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* High-level WebRTC peer connection manager with state-based lifecycle
|
|
287
|
-
* Handles offer/answer exchange, ICE candidates, timeouts, and error recovery
|
|
288
|
-
*/
|
|
289
|
-
export default class RondevuPeer extends EventEmitter {
|
|
290
|
-
/**
|
|
291
|
-
* Current connection state name
|
|
292
|
-
*/
|
|
293
|
-
get stateName() {
|
|
294
|
-
return this._state.name;
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Current state object (internal use)
|
|
298
|
-
*/
|
|
299
|
-
get state() {
|
|
300
|
-
return this._state;
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* RTCPeerConnection state
|
|
304
|
-
*/
|
|
305
|
-
get connectionState() {
|
|
306
|
-
return this.pc.connectionState;
|
|
307
|
-
}
|
|
308
|
-
constructor(offersApi, rtcConfig = {
|
|
309
|
-
iceServers: [
|
|
310
|
-
{ urls: 'stun:stun.l.google.com:19302' },
|
|
311
|
-
{ urls: 'stun:stun1.l.google.com:19302' }
|
|
312
|
-
]
|
|
313
|
-
}) {
|
|
314
|
-
super();
|
|
315
|
-
this.offersApi = offersApi;
|
|
316
|
-
this.pc = new RTCPeerConnection(rtcConfig);
|
|
317
|
-
this._state = new IdleState(this);
|
|
318
|
-
this.setupPeerConnection();
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Set up peer connection event handlers
|
|
322
|
-
*/
|
|
323
|
-
setupPeerConnection() {
|
|
324
|
-
this.pc.onconnectionstatechange = () => {
|
|
325
|
-
switch (this.pc.connectionState) {
|
|
326
|
-
case 'connected':
|
|
327
|
-
this.setState(new ConnectedState(this));
|
|
328
|
-
this.emitEvent('connected');
|
|
329
|
-
break;
|
|
330
|
-
case 'disconnected':
|
|
331
|
-
this.emitEvent('disconnected');
|
|
332
|
-
break;
|
|
333
|
-
case 'failed':
|
|
334
|
-
this.setState(new FailedState(this, new Error('Connection failed')));
|
|
335
|
-
break;
|
|
336
|
-
case 'closed':
|
|
337
|
-
this.setState(new ClosedState(this));
|
|
338
|
-
this.emitEvent('disconnected');
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
this.pc.ondatachannel = (event) => {
|
|
343
|
-
this.emitEvent('datachannel', event.channel);
|
|
344
|
-
};
|
|
345
|
-
this.pc.ontrack = (event) => {
|
|
346
|
-
this.emitEvent('track', event);
|
|
347
|
-
};
|
|
348
|
-
this.pc.onicecandidateerror = (event) => {
|
|
349
|
-
console.error('ICE candidate error:', event);
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Set new state and emit state change event
|
|
354
|
-
*/
|
|
355
|
-
setState(newState) {
|
|
356
|
-
this._state.cleanup();
|
|
357
|
-
this._state = newState;
|
|
358
|
-
this.emitEvent('state', newState.name);
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* Emit event (exposed for PeerState classes)
|
|
362
|
-
* @internal
|
|
363
|
-
*/
|
|
364
|
-
emitEvent(event, ...args) {
|
|
365
|
-
this.emit(event, ...args);
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Create an offer and advertise on topics
|
|
369
|
-
*/
|
|
370
|
-
async createOffer(options) {
|
|
371
|
-
return this._state.createOffer(options);
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Answer an existing offer
|
|
375
|
-
*/
|
|
376
|
-
async answer(offerId, offerSdp, options) {
|
|
377
|
-
return this._state.answer(offerId, offerSdp, options);
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Add a media track to the connection
|
|
381
|
-
*/
|
|
382
|
-
addTrack(track, ...streams) {
|
|
383
|
-
return this.pc.addTrack(track, ...streams);
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Close the connection and clean up
|
|
387
|
-
*/
|
|
388
|
-
close() {
|
|
389
|
-
this._state.close();
|
|
390
|
-
this.removeAllListeners();
|
|
391
|
-
}
|
|
392
|
-
}
|