@voicemaster/core 1.0.0 → 1.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/dist/index.d.mts +17 -35
- package/dist/index.d.ts +17 -35
- package/dist/index.js +40 -173
- package/dist/index.mjs +40 -173
- package/package.json +10 -3
- package/src/VoiceClient.ts +61 -229
package/dist/index.d.mts
CHANGED
|
@@ -5,60 +5,42 @@ interface VoiceClientConfig {
|
|
|
5
5
|
autoConnect?: boolean;
|
|
6
6
|
iceServers?: RTCIceServer[];
|
|
7
7
|
}
|
|
8
|
-
interface VoiceEvents {
|
|
9
|
-
connected: () => void;
|
|
10
|
-
disconnected: () => void;
|
|
11
|
-
remoteStream: (stream: MediaStream) => void;
|
|
12
|
-
localStream: (stream: MediaStream) => void;
|
|
13
|
-
error: (error: Error) => void;
|
|
14
|
-
userJoined: (userId: string) => void;
|
|
15
|
-
userLeft: (userId: string) => void;
|
|
16
|
-
speaking: (userId: string) => void;
|
|
17
|
-
stoppedSpeaking: (userId: string) => void;
|
|
18
|
-
}
|
|
19
|
-
type EventHandler<T extends keyof VoiceEvents> = VoiceEvents[T];
|
|
20
8
|
declare class VoiceClient {
|
|
21
9
|
private ws;
|
|
22
|
-
private
|
|
10
|
+
private peer;
|
|
23
11
|
private localStream;
|
|
24
|
-
private
|
|
12
|
+
private remoteStream;
|
|
25
13
|
private eventHandlers;
|
|
26
|
-
private userId;
|
|
27
|
-
private roomId;
|
|
28
14
|
private signalingUrl;
|
|
15
|
+
private roomId;
|
|
16
|
+
private userId;
|
|
29
17
|
private iceServers;
|
|
30
|
-
private
|
|
31
|
-
private audioContext;
|
|
32
|
-
private sourceNode;
|
|
33
|
-
private analyserNode;
|
|
34
|
-
private speakingThreshold;
|
|
35
|
-
private isSpeaking;
|
|
36
|
-
private reconnectAttempts;
|
|
37
|
-
private maxReconnectAttempts;
|
|
38
|
-
private isConnected;
|
|
18
|
+
private isConnectedFlag;
|
|
39
19
|
constructor(config: VoiceClientConfig);
|
|
40
|
-
on
|
|
41
|
-
off<K extends keyof VoiceEvents>(event: K, handler: EventHandler<K>): void;
|
|
20
|
+
on(event: string, callback: Function): void;
|
|
42
21
|
private emit;
|
|
43
22
|
connect(): Promise<void>;
|
|
44
23
|
private initMicrophone;
|
|
45
|
-
private setupSpeakingDetection;
|
|
46
|
-
private broadcastSpeakingStatus;
|
|
47
24
|
private initWebSocket;
|
|
48
|
-
private handleReconnect;
|
|
49
25
|
private handleSignalingMessage;
|
|
50
26
|
private initPeer;
|
|
51
27
|
private handlePeerSignal;
|
|
52
|
-
private closePeer;
|
|
53
28
|
private send;
|
|
54
29
|
disconnect(): void;
|
|
55
30
|
toggleMute(): void;
|
|
56
31
|
isMuted(): boolean;
|
|
57
|
-
setSpeakingThreshold(threshold: number): void;
|
|
58
|
-
getRemoteStream(userId?: string): MediaStream | null;
|
|
59
|
-
getPeers(): string[];
|
|
60
32
|
getUserId(): string;
|
|
61
|
-
|
|
33
|
+
}
|
|
34
|
+
interface VoiceEvents {
|
|
35
|
+
connected: () => void;
|
|
36
|
+
disconnected: () => void;
|
|
37
|
+
remoteStream: (stream: MediaStream) => void;
|
|
38
|
+
localStream: (stream: MediaStream) => void;
|
|
39
|
+
error: (error: Error) => void;
|
|
40
|
+
userJoined: (userId: string) => void;
|
|
41
|
+
userLeft: (userId: string) => void;
|
|
42
|
+
speaking: (userId: string) => void;
|
|
43
|
+
stoppedSpeaking: (userId: string) => void;
|
|
62
44
|
}
|
|
63
45
|
|
|
64
46
|
export { VoiceClient, type VoiceClientConfig, type VoiceEvents };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,60 +5,42 @@ interface VoiceClientConfig {
|
|
|
5
5
|
autoConnect?: boolean;
|
|
6
6
|
iceServers?: RTCIceServer[];
|
|
7
7
|
}
|
|
8
|
-
interface VoiceEvents {
|
|
9
|
-
connected: () => void;
|
|
10
|
-
disconnected: () => void;
|
|
11
|
-
remoteStream: (stream: MediaStream) => void;
|
|
12
|
-
localStream: (stream: MediaStream) => void;
|
|
13
|
-
error: (error: Error) => void;
|
|
14
|
-
userJoined: (userId: string) => void;
|
|
15
|
-
userLeft: (userId: string) => void;
|
|
16
|
-
speaking: (userId: string) => void;
|
|
17
|
-
stoppedSpeaking: (userId: string) => void;
|
|
18
|
-
}
|
|
19
|
-
type EventHandler<T extends keyof VoiceEvents> = VoiceEvents[T];
|
|
20
8
|
declare class VoiceClient {
|
|
21
9
|
private ws;
|
|
22
|
-
private
|
|
10
|
+
private peer;
|
|
23
11
|
private localStream;
|
|
24
|
-
private
|
|
12
|
+
private remoteStream;
|
|
25
13
|
private eventHandlers;
|
|
26
|
-
private userId;
|
|
27
|
-
private roomId;
|
|
28
14
|
private signalingUrl;
|
|
15
|
+
private roomId;
|
|
16
|
+
private userId;
|
|
29
17
|
private iceServers;
|
|
30
|
-
private
|
|
31
|
-
private audioContext;
|
|
32
|
-
private sourceNode;
|
|
33
|
-
private analyserNode;
|
|
34
|
-
private speakingThreshold;
|
|
35
|
-
private isSpeaking;
|
|
36
|
-
private reconnectAttempts;
|
|
37
|
-
private maxReconnectAttempts;
|
|
38
|
-
private isConnected;
|
|
18
|
+
private isConnectedFlag;
|
|
39
19
|
constructor(config: VoiceClientConfig);
|
|
40
|
-
on
|
|
41
|
-
off<K extends keyof VoiceEvents>(event: K, handler: EventHandler<K>): void;
|
|
20
|
+
on(event: string, callback: Function): void;
|
|
42
21
|
private emit;
|
|
43
22
|
connect(): Promise<void>;
|
|
44
23
|
private initMicrophone;
|
|
45
|
-
private setupSpeakingDetection;
|
|
46
|
-
private broadcastSpeakingStatus;
|
|
47
24
|
private initWebSocket;
|
|
48
|
-
private handleReconnect;
|
|
49
25
|
private handleSignalingMessage;
|
|
50
26
|
private initPeer;
|
|
51
27
|
private handlePeerSignal;
|
|
52
|
-
private closePeer;
|
|
53
28
|
private send;
|
|
54
29
|
disconnect(): void;
|
|
55
30
|
toggleMute(): void;
|
|
56
31
|
isMuted(): boolean;
|
|
57
|
-
setSpeakingThreshold(threshold: number): void;
|
|
58
|
-
getRemoteStream(userId?: string): MediaStream | null;
|
|
59
|
-
getPeers(): string[];
|
|
60
32
|
getUserId(): string;
|
|
61
|
-
|
|
33
|
+
}
|
|
34
|
+
interface VoiceEvents {
|
|
35
|
+
connected: () => void;
|
|
36
|
+
disconnected: () => void;
|
|
37
|
+
remoteStream: (stream: MediaStream) => void;
|
|
38
|
+
localStream: (stream: MediaStream) => void;
|
|
39
|
+
error: (error: Error) => void;
|
|
40
|
+
userJoined: (userId: string) => void;
|
|
41
|
+
userLeft: (userId: string) => void;
|
|
42
|
+
speaking: (userId: string) => void;
|
|
43
|
+
stoppedSpeaking: (userId: string) => void;
|
|
62
44
|
}
|
|
63
45
|
|
|
64
46
|
export { VoiceClient, type VoiceClientConfig, type VoiceEvents };
|
package/dist/index.js
CHANGED
|
@@ -39,50 +39,29 @@ var import_simple_peer = __toESM(require("simple-peer"));
|
|
|
39
39
|
var VoiceClient = class {
|
|
40
40
|
constructor(config) {
|
|
41
41
|
this.ws = null;
|
|
42
|
-
this.
|
|
42
|
+
this.peer = null;
|
|
43
43
|
this.localStream = null;
|
|
44
|
-
this.
|
|
45
|
-
this.eventHandlers =
|
|
46
|
-
this.
|
|
47
|
-
this.audioContext = null;
|
|
48
|
-
this.sourceNode = null;
|
|
49
|
-
this.analyserNode = null;
|
|
50
|
-
this.speakingThreshold = 0.05;
|
|
51
|
-
this.isSpeaking = false;
|
|
52
|
-
this.reconnectAttempts = 0;
|
|
53
|
-
this.maxReconnectAttempts = 5;
|
|
54
|
-
this.isConnected = false;
|
|
55
|
-
this.userId = config.userId;
|
|
56
|
-
this.roomId = config.roomId;
|
|
44
|
+
this.remoteStream = null;
|
|
45
|
+
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
46
|
+
this.isConnectedFlag = false;
|
|
57
47
|
this.signalingUrl = config.signalingUrl;
|
|
48
|
+
this.roomId = config.roomId;
|
|
49
|
+
this.userId = config.userId;
|
|
58
50
|
this.iceServers = config.iceServers || [
|
|
59
|
-
{ urls: "stun:stun.l.google.com:19302" }
|
|
60
|
-
{ urls: "stun:stun.relay.metered.ca:80" },
|
|
61
|
-
{
|
|
62
|
-
urls: "turn:openrelay.metered.ca:80",
|
|
63
|
-
username: "openrelayproject",
|
|
64
|
-
credential: "openrelayproject"
|
|
65
|
-
}
|
|
51
|
+
{ urls: "stun:stun.l.google.com:19302" }
|
|
66
52
|
];
|
|
67
53
|
if (config.autoConnect !== false) {
|
|
68
54
|
this.connect();
|
|
69
55
|
}
|
|
70
56
|
}
|
|
71
|
-
on(event,
|
|
72
|
-
if (!this.eventHandlers
|
|
73
|
-
this.eventHandlers
|
|
74
|
-
}
|
|
75
|
-
this.eventHandlers[event].push(handler);
|
|
76
|
-
}
|
|
77
|
-
off(event, handler) {
|
|
78
|
-
const handlers = this.eventHandlers[event];
|
|
79
|
-
if (handlers) {
|
|
80
|
-
const index = handlers.indexOf(handler);
|
|
81
|
-
if (index !== -1) handlers.splice(index, 1);
|
|
57
|
+
on(event, callback) {
|
|
58
|
+
if (!this.eventHandlers.has(event)) {
|
|
59
|
+
this.eventHandlers.set(event, []);
|
|
82
60
|
}
|
|
61
|
+
this.eventHandlers.get(event).push(callback);
|
|
83
62
|
}
|
|
84
63
|
emit(event, ...args) {
|
|
85
|
-
const handlers = this.eventHandlers
|
|
64
|
+
const handlers = this.eventHandlers.get(event);
|
|
86
65
|
if (handlers) {
|
|
87
66
|
handlers.forEach((handler) => handler(...args));
|
|
88
67
|
}
|
|
@@ -93,7 +72,6 @@ var VoiceClient = class {
|
|
|
93
72
|
this.initWebSocket();
|
|
94
73
|
} catch (error) {
|
|
95
74
|
this.emit("error", error);
|
|
96
|
-
this.handleReconnect();
|
|
97
75
|
}
|
|
98
76
|
}
|
|
99
77
|
async initMicrophone() {
|
|
@@ -101,159 +79,74 @@ var VoiceClient = class {
|
|
|
101
79
|
audio: {
|
|
102
80
|
echoCancellation: true,
|
|
103
81
|
noiseSuppression: true,
|
|
104
|
-
autoGainControl: true
|
|
105
|
-
sampleRate: 48e3,
|
|
106
|
-
channelCount: 1
|
|
82
|
+
autoGainControl: true
|
|
107
83
|
}
|
|
108
84
|
});
|
|
109
85
|
this.emit("localStream", this.localStream);
|
|
110
|
-
this.setupSpeakingDetection();
|
|
111
|
-
}
|
|
112
|
-
setupSpeakingDetection() {
|
|
113
|
-
if (!this.localStream) return;
|
|
114
|
-
this.audioContext = new AudioContext();
|
|
115
|
-
this.sourceNode = this.audioContext.createMediaStreamSource(this.localStream);
|
|
116
|
-
this.analyserNode = this.audioContext.createAnalyser();
|
|
117
|
-
this.analyserNode.fftSize = 256;
|
|
118
|
-
this.sourceNode.connect(this.analyserNode);
|
|
119
|
-
this.sourceNode.connect(this.audioContext.destination);
|
|
120
|
-
const dataArray = new Uint8Array(this.analyserNode.frequencyBinCount);
|
|
121
|
-
this.speakingDetectionInterval = setInterval(() => {
|
|
122
|
-
if (!this.analyserNode) return;
|
|
123
|
-
this.analyserNode.getByteTimeDomainData(dataArray);
|
|
124
|
-
let maxSample = 0;
|
|
125
|
-
for (let i = 0; i < dataArray.length; i++) {
|
|
126
|
-
const sample = Math.abs(dataArray[i] / 128 - 1);
|
|
127
|
-
if (sample > maxSample) maxSample = sample;
|
|
128
|
-
}
|
|
129
|
-
const isCurrentlySpeaking = maxSample > this.speakingThreshold;
|
|
130
|
-
if (isCurrentlySpeaking && !this.isSpeaking) {
|
|
131
|
-
this.isSpeaking = true;
|
|
132
|
-
this.broadcastSpeakingStatus(true);
|
|
133
|
-
this.emit("speaking", this.userId);
|
|
134
|
-
} else if (!isCurrentlySpeaking && this.isSpeaking) {
|
|
135
|
-
this.isSpeaking = false;
|
|
136
|
-
this.broadcastSpeakingStatus(false);
|
|
137
|
-
this.emit("stoppedSpeaking", this.userId);
|
|
138
|
-
}
|
|
139
|
-
}, 100);
|
|
140
|
-
}
|
|
141
|
-
broadcastSpeakingStatus(isSpeaking) {
|
|
142
|
-
this.peers.forEach((peer) => {
|
|
143
|
-
if (peer && peer.connected) {
|
|
144
|
-
peer.send(JSON.stringify({
|
|
145
|
-
type: "speaking",
|
|
146
|
-
userId: this.userId,
|
|
147
|
-
isSpeaking
|
|
148
|
-
}));
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
86
|
}
|
|
152
87
|
initWebSocket() {
|
|
153
88
|
const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
|
|
154
89
|
this.ws = new WebSocket(url);
|
|
155
90
|
this.ws.onopen = () => {
|
|
156
|
-
this.
|
|
157
|
-
this.isConnected = true;
|
|
158
|
-
this.send({
|
|
159
|
-
type: "join",
|
|
160
|
-
roomId: this.roomId,
|
|
161
|
-
userId: this.userId
|
|
162
|
-
});
|
|
91
|
+
this.send({ type: "join", roomId: this.roomId, userId: this.userId });
|
|
163
92
|
};
|
|
164
93
|
this.ws.onmessage = (event) => {
|
|
165
94
|
const message = JSON.parse(event.data);
|
|
166
95
|
this.handleSignalingMessage(message);
|
|
167
96
|
};
|
|
168
97
|
this.ws.onclose = () => {
|
|
169
|
-
this.isConnected = false;
|
|
170
|
-
this.handleReconnect();
|
|
171
|
-
};
|
|
172
|
-
this.ws.onerror = (error) => {
|
|
173
|
-
this.emit("error", new Error("WebSocket error"));
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
handleReconnect() {
|
|
177
|
-
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
178
|
-
this.reconnectAttempts++;
|
|
179
|
-
setTimeout(() => {
|
|
180
|
-
this.initWebSocket();
|
|
181
|
-
}, 1e3 * Math.min(30, this.reconnectAttempts));
|
|
182
|
-
} else {
|
|
183
98
|
this.emit("disconnected");
|
|
184
|
-
}
|
|
99
|
+
};
|
|
185
100
|
}
|
|
186
101
|
handleSignalingMessage(message) {
|
|
187
102
|
switch (message.type) {
|
|
188
103
|
case "user-joined":
|
|
189
|
-
|
|
190
|
-
|
|
104
|
+
if (message.userId !== this.userId) {
|
|
105
|
+
this.initPeer(true);
|
|
106
|
+
this.emit("userJoined", message.userId);
|
|
107
|
+
}
|
|
191
108
|
break;
|
|
192
109
|
case "signal":
|
|
193
110
|
this.handlePeerSignal(message.userId, message.payload);
|
|
194
111
|
break;
|
|
195
112
|
case "user-left":
|
|
196
|
-
this.closePeer(message.userId);
|
|
197
113
|
this.emit("userLeft", message.userId);
|
|
198
114
|
break;
|
|
199
115
|
}
|
|
200
116
|
}
|
|
201
|
-
initPeer(
|
|
202
|
-
if (this.
|
|
203
|
-
|
|
117
|
+
initPeer(initiator) {
|
|
118
|
+
if (this.peer) return;
|
|
119
|
+
this.peer = new import_simple_peer.default({
|
|
204
120
|
initiator,
|
|
205
121
|
trickle: true,
|
|
206
|
-
stream: this.localStream,
|
|
122
|
+
stream: this.localStream || void 0,
|
|
207
123
|
config: { iceServers: this.iceServers }
|
|
208
124
|
});
|
|
209
|
-
peer.on("signal", (data) => {
|
|
125
|
+
this.peer.on("signal", (data) => {
|
|
210
126
|
this.send({
|
|
211
127
|
type: "signal",
|
|
212
|
-
userId:
|
|
128
|
+
userId: this.userId,
|
|
129
|
+
roomId: this.roomId,
|
|
213
130
|
payload: data
|
|
214
131
|
});
|
|
215
132
|
});
|
|
216
|
-
peer.on("stream", (stream) => {
|
|
217
|
-
this.
|
|
133
|
+
this.peer.on("stream", (stream) => {
|
|
134
|
+
this.remoteStream = stream;
|
|
218
135
|
this.emit("remoteStream", stream);
|
|
219
136
|
});
|
|
220
|
-
peer.on("
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (parsed.type === "speaking") {
|
|
224
|
-
if (parsed.isSpeaking) {
|
|
225
|
-
this.emit("speaking", parsed.userId);
|
|
226
|
-
} else {
|
|
227
|
-
this.emit("stoppedSpeaking", parsed.userId);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
} catch (e) {
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
peer.on("error", (err) => {
|
|
234
|
-
this.emit("error", err);
|
|
137
|
+
this.peer.on("connect", () => {
|
|
138
|
+
this.isConnectedFlag = true;
|
|
139
|
+
this.emit("connected");
|
|
235
140
|
});
|
|
236
|
-
peer.on("
|
|
237
|
-
|
|
141
|
+
this.peer.on("error", (err) => {
|
|
142
|
+
console.error("Peer error:", err);
|
|
238
143
|
});
|
|
239
|
-
this.peers.set(targetUserId, peer);
|
|
240
144
|
}
|
|
241
145
|
handlePeerSignal(targetUserId, signal) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
this.initPeer(targetUserId, false);
|
|
245
|
-
peer = this.peers.get(targetUserId);
|
|
246
|
-
}
|
|
247
|
-
if (peer) {
|
|
248
|
-
peer.signal(signal);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
closePeer(userId) {
|
|
252
|
-
const peer = this.peers.get(userId);
|
|
253
|
-
if (peer) {
|
|
254
|
-
peer.destroy();
|
|
255
|
-
this.peers.delete(userId);
|
|
146
|
+
if (!this.peer) {
|
|
147
|
+
this.initPeer(false);
|
|
256
148
|
}
|
|
149
|
+
this.peer?.signal(signal);
|
|
257
150
|
}
|
|
258
151
|
send(data) {
|
|
259
152
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -261,22 +154,13 @@ var VoiceClient = class {
|
|
|
261
154
|
}
|
|
262
155
|
}
|
|
263
156
|
disconnect() {
|
|
264
|
-
if (this.speakingDetectionInterval) {
|
|
265
|
-
clearInterval(this.speakingDetectionInterval);
|
|
266
|
-
}
|
|
267
|
-
if (this.audioContext) {
|
|
268
|
-
this.audioContext.close();
|
|
269
|
-
}
|
|
270
|
-
this.peers.forEach((peer) => {
|
|
271
|
-
peer.destroy();
|
|
272
|
-
});
|
|
273
|
-
this.peers.clear();
|
|
274
|
-
if (this.localStream) {
|
|
275
|
-
this.localStream.getTracks().forEach((track) => track.stop());
|
|
276
|
-
}
|
|
277
157
|
if (this.ws) {
|
|
158
|
+
this.send({ type: "leave", roomId: this.roomId, userId: this.userId });
|
|
278
159
|
this.ws.close();
|
|
279
160
|
}
|
|
161
|
+
this.peer?.destroy();
|
|
162
|
+
this.localStream?.getTracks().forEach((track) => track.stop());
|
|
163
|
+
this.isConnectedFlag = false;
|
|
280
164
|
this.emit("disconnected");
|
|
281
165
|
}
|
|
282
166
|
toggleMute() {
|
|
@@ -290,26 +174,9 @@ var VoiceClient = class {
|
|
|
290
174
|
isMuted() {
|
|
291
175
|
return this.localStream?.getAudioTracks()[0]?.enabled === false;
|
|
292
176
|
}
|
|
293
|
-
setSpeakingThreshold(threshold) {
|
|
294
|
-
this.speakingThreshold = Math.min(1, Math.max(0, threshold));
|
|
295
|
-
}
|
|
296
|
-
getRemoteStream(userId) {
|
|
297
|
-
if (userId) {
|
|
298
|
-
const stream = this.remoteStreams.get(userId);
|
|
299
|
-
return stream || null;
|
|
300
|
-
}
|
|
301
|
-
const firstStream = this.remoteStreams.values().next().value;
|
|
302
|
-
return firstStream || null;
|
|
303
|
-
}
|
|
304
|
-
getPeers() {
|
|
305
|
-
return Array.from(this.peers.keys());
|
|
306
|
-
}
|
|
307
177
|
getUserId() {
|
|
308
178
|
return this.userId;
|
|
309
179
|
}
|
|
310
|
-
isConnectedToRoom() {
|
|
311
|
-
return this.isConnected;
|
|
312
|
-
}
|
|
313
180
|
};
|
|
314
181
|
// Annotate the CommonJS export names for ESM import in node:
|
|
315
182
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
|
@@ -3,50 +3,29 @@ import Peer from "simple-peer";
|
|
|
3
3
|
var VoiceClient = class {
|
|
4
4
|
constructor(config) {
|
|
5
5
|
this.ws = null;
|
|
6
|
-
this.
|
|
6
|
+
this.peer = null;
|
|
7
7
|
this.localStream = null;
|
|
8
|
-
this.
|
|
9
|
-
this.eventHandlers =
|
|
10
|
-
this.
|
|
11
|
-
this.audioContext = null;
|
|
12
|
-
this.sourceNode = null;
|
|
13
|
-
this.analyserNode = null;
|
|
14
|
-
this.speakingThreshold = 0.05;
|
|
15
|
-
this.isSpeaking = false;
|
|
16
|
-
this.reconnectAttempts = 0;
|
|
17
|
-
this.maxReconnectAttempts = 5;
|
|
18
|
-
this.isConnected = false;
|
|
19
|
-
this.userId = config.userId;
|
|
20
|
-
this.roomId = config.roomId;
|
|
8
|
+
this.remoteStream = null;
|
|
9
|
+
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
10
|
+
this.isConnectedFlag = false;
|
|
21
11
|
this.signalingUrl = config.signalingUrl;
|
|
12
|
+
this.roomId = config.roomId;
|
|
13
|
+
this.userId = config.userId;
|
|
22
14
|
this.iceServers = config.iceServers || [
|
|
23
|
-
{ urls: "stun:stun.l.google.com:19302" }
|
|
24
|
-
{ urls: "stun:stun.relay.metered.ca:80" },
|
|
25
|
-
{
|
|
26
|
-
urls: "turn:openrelay.metered.ca:80",
|
|
27
|
-
username: "openrelayproject",
|
|
28
|
-
credential: "openrelayproject"
|
|
29
|
-
}
|
|
15
|
+
{ urls: "stun:stun.l.google.com:19302" }
|
|
30
16
|
];
|
|
31
17
|
if (config.autoConnect !== false) {
|
|
32
18
|
this.connect();
|
|
33
19
|
}
|
|
34
20
|
}
|
|
35
|
-
on(event,
|
|
36
|
-
if (!this.eventHandlers
|
|
37
|
-
this.eventHandlers
|
|
38
|
-
}
|
|
39
|
-
this.eventHandlers[event].push(handler);
|
|
40
|
-
}
|
|
41
|
-
off(event, handler) {
|
|
42
|
-
const handlers = this.eventHandlers[event];
|
|
43
|
-
if (handlers) {
|
|
44
|
-
const index = handlers.indexOf(handler);
|
|
45
|
-
if (index !== -1) handlers.splice(index, 1);
|
|
21
|
+
on(event, callback) {
|
|
22
|
+
if (!this.eventHandlers.has(event)) {
|
|
23
|
+
this.eventHandlers.set(event, []);
|
|
46
24
|
}
|
|
25
|
+
this.eventHandlers.get(event).push(callback);
|
|
47
26
|
}
|
|
48
27
|
emit(event, ...args) {
|
|
49
|
-
const handlers = this.eventHandlers
|
|
28
|
+
const handlers = this.eventHandlers.get(event);
|
|
50
29
|
if (handlers) {
|
|
51
30
|
handlers.forEach((handler) => handler(...args));
|
|
52
31
|
}
|
|
@@ -57,7 +36,6 @@ var VoiceClient = class {
|
|
|
57
36
|
this.initWebSocket();
|
|
58
37
|
} catch (error) {
|
|
59
38
|
this.emit("error", error);
|
|
60
|
-
this.handleReconnect();
|
|
61
39
|
}
|
|
62
40
|
}
|
|
63
41
|
async initMicrophone() {
|
|
@@ -65,159 +43,74 @@ var VoiceClient = class {
|
|
|
65
43
|
audio: {
|
|
66
44
|
echoCancellation: true,
|
|
67
45
|
noiseSuppression: true,
|
|
68
|
-
autoGainControl: true
|
|
69
|
-
sampleRate: 48e3,
|
|
70
|
-
channelCount: 1
|
|
46
|
+
autoGainControl: true
|
|
71
47
|
}
|
|
72
48
|
});
|
|
73
49
|
this.emit("localStream", this.localStream);
|
|
74
|
-
this.setupSpeakingDetection();
|
|
75
|
-
}
|
|
76
|
-
setupSpeakingDetection() {
|
|
77
|
-
if (!this.localStream) return;
|
|
78
|
-
this.audioContext = new AudioContext();
|
|
79
|
-
this.sourceNode = this.audioContext.createMediaStreamSource(this.localStream);
|
|
80
|
-
this.analyserNode = this.audioContext.createAnalyser();
|
|
81
|
-
this.analyserNode.fftSize = 256;
|
|
82
|
-
this.sourceNode.connect(this.analyserNode);
|
|
83
|
-
this.sourceNode.connect(this.audioContext.destination);
|
|
84
|
-
const dataArray = new Uint8Array(this.analyserNode.frequencyBinCount);
|
|
85
|
-
this.speakingDetectionInterval = setInterval(() => {
|
|
86
|
-
if (!this.analyserNode) return;
|
|
87
|
-
this.analyserNode.getByteTimeDomainData(dataArray);
|
|
88
|
-
let maxSample = 0;
|
|
89
|
-
for (let i = 0; i < dataArray.length; i++) {
|
|
90
|
-
const sample = Math.abs(dataArray[i] / 128 - 1);
|
|
91
|
-
if (sample > maxSample) maxSample = sample;
|
|
92
|
-
}
|
|
93
|
-
const isCurrentlySpeaking = maxSample > this.speakingThreshold;
|
|
94
|
-
if (isCurrentlySpeaking && !this.isSpeaking) {
|
|
95
|
-
this.isSpeaking = true;
|
|
96
|
-
this.broadcastSpeakingStatus(true);
|
|
97
|
-
this.emit("speaking", this.userId);
|
|
98
|
-
} else if (!isCurrentlySpeaking && this.isSpeaking) {
|
|
99
|
-
this.isSpeaking = false;
|
|
100
|
-
this.broadcastSpeakingStatus(false);
|
|
101
|
-
this.emit("stoppedSpeaking", this.userId);
|
|
102
|
-
}
|
|
103
|
-
}, 100);
|
|
104
|
-
}
|
|
105
|
-
broadcastSpeakingStatus(isSpeaking) {
|
|
106
|
-
this.peers.forEach((peer) => {
|
|
107
|
-
if (peer && peer.connected) {
|
|
108
|
-
peer.send(JSON.stringify({
|
|
109
|
-
type: "speaking",
|
|
110
|
-
userId: this.userId,
|
|
111
|
-
isSpeaking
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
50
|
}
|
|
116
51
|
initWebSocket() {
|
|
117
52
|
const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
|
|
118
53
|
this.ws = new WebSocket(url);
|
|
119
54
|
this.ws.onopen = () => {
|
|
120
|
-
this.
|
|
121
|
-
this.isConnected = true;
|
|
122
|
-
this.send({
|
|
123
|
-
type: "join",
|
|
124
|
-
roomId: this.roomId,
|
|
125
|
-
userId: this.userId
|
|
126
|
-
});
|
|
55
|
+
this.send({ type: "join", roomId: this.roomId, userId: this.userId });
|
|
127
56
|
};
|
|
128
57
|
this.ws.onmessage = (event) => {
|
|
129
58
|
const message = JSON.parse(event.data);
|
|
130
59
|
this.handleSignalingMessage(message);
|
|
131
60
|
};
|
|
132
61
|
this.ws.onclose = () => {
|
|
133
|
-
this.isConnected = false;
|
|
134
|
-
this.handleReconnect();
|
|
135
|
-
};
|
|
136
|
-
this.ws.onerror = (error) => {
|
|
137
|
-
this.emit("error", new Error("WebSocket error"));
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
handleReconnect() {
|
|
141
|
-
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
142
|
-
this.reconnectAttempts++;
|
|
143
|
-
setTimeout(() => {
|
|
144
|
-
this.initWebSocket();
|
|
145
|
-
}, 1e3 * Math.min(30, this.reconnectAttempts));
|
|
146
|
-
} else {
|
|
147
62
|
this.emit("disconnected");
|
|
148
|
-
}
|
|
63
|
+
};
|
|
149
64
|
}
|
|
150
65
|
handleSignalingMessage(message) {
|
|
151
66
|
switch (message.type) {
|
|
152
67
|
case "user-joined":
|
|
153
|
-
|
|
154
|
-
|
|
68
|
+
if (message.userId !== this.userId) {
|
|
69
|
+
this.initPeer(true);
|
|
70
|
+
this.emit("userJoined", message.userId);
|
|
71
|
+
}
|
|
155
72
|
break;
|
|
156
73
|
case "signal":
|
|
157
74
|
this.handlePeerSignal(message.userId, message.payload);
|
|
158
75
|
break;
|
|
159
76
|
case "user-left":
|
|
160
|
-
this.closePeer(message.userId);
|
|
161
77
|
this.emit("userLeft", message.userId);
|
|
162
78
|
break;
|
|
163
79
|
}
|
|
164
80
|
}
|
|
165
|
-
initPeer(
|
|
166
|
-
if (this.
|
|
167
|
-
|
|
81
|
+
initPeer(initiator) {
|
|
82
|
+
if (this.peer) return;
|
|
83
|
+
this.peer = new Peer({
|
|
168
84
|
initiator,
|
|
169
85
|
trickle: true,
|
|
170
|
-
stream: this.localStream,
|
|
86
|
+
stream: this.localStream || void 0,
|
|
171
87
|
config: { iceServers: this.iceServers }
|
|
172
88
|
});
|
|
173
|
-
peer.on("signal", (data) => {
|
|
89
|
+
this.peer.on("signal", (data) => {
|
|
174
90
|
this.send({
|
|
175
91
|
type: "signal",
|
|
176
|
-
userId:
|
|
92
|
+
userId: this.userId,
|
|
93
|
+
roomId: this.roomId,
|
|
177
94
|
payload: data
|
|
178
95
|
});
|
|
179
96
|
});
|
|
180
|
-
peer.on("stream", (stream) => {
|
|
181
|
-
this.
|
|
97
|
+
this.peer.on("stream", (stream) => {
|
|
98
|
+
this.remoteStream = stream;
|
|
182
99
|
this.emit("remoteStream", stream);
|
|
183
100
|
});
|
|
184
|
-
peer.on("
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (parsed.type === "speaking") {
|
|
188
|
-
if (parsed.isSpeaking) {
|
|
189
|
-
this.emit("speaking", parsed.userId);
|
|
190
|
-
} else {
|
|
191
|
-
this.emit("stoppedSpeaking", parsed.userId);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
} catch (e) {
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
peer.on("error", (err) => {
|
|
198
|
-
this.emit("error", err);
|
|
101
|
+
this.peer.on("connect", () => {
|
|
102
|
+
this.isConnectedFlag = true;
|
|
103
|
+
this.emit("connected");
|
|
199
104
|
});
|
|
200
|
-
peer.on("
|
|
201
|
-
|
|
105
|
+
this.peer.on("error", (err) => {
|
|
106
|
+
console.error("Peer error:", err);
|
|
202
107
|
});
|
|
203
|
-
this.peers.set(targetUserId, peer);
|
|
204
108
|
}
|
|
205
109
|
handlePeerSignal(targetUserId, signal) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
this.initPeer(targetUserId, false);
|
|
209
|
-
peer = this.peers.get(targetUserId);
|
|
210
|
-
}
|
|
211
|
-
if (peer) {
|
|
212
|
-
peer.signal(signal);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
closePeer(userId) {
|
|
216
|
-
const peer = this.peers.get(userId);
|
|
217
|
-
if (peer) {
|
|
218
|
-
peer.destroy();
|
|
219
|
-
this.peers.delete(userId);
|
|
110
|
+
if (!this.peer) {
|
|
111
|
+
this.initPeer(false);
|
|
220
112
|
}
|
|
113
|
+
this.peer?.signal(signal);
|
|
221
114
|
}
|
|
222
115
|
send(data) {
|
|
223
116
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -225,22 +118,13 @@ var VoiceClient = class {
|
|
|
225
118
|
}
|
|
226
119
|
}
|
|
227
120
|
disconnect() {
|
|
228
|
-
if (this.speakingDetectionInterval) {
|
|
229
|
-
clearInterval(this.speakingDetectionInterval);
|
|
230
|
-
}
|
|
231
|
-
if (this.audioContext) {
|
|
232
|
-
this.audioContext.close();
|
|
233
|
-
}
|
|
234
|
-
this.peers.forEach((peer) => {
|
|
235
|
-
peer.destroy();
|
|
236
|
-
});
|
|
237
|
-
this.peers.clear();
|
|
238
|
-
if (this.localStream) {
|
|
239
|
-
this.localStream.getTracks().forEach((track) => track.stop());
|
|
240
|
-
}
|
|
241
121
|
if (this.ws) {
|
|
122
|
+
this.send({ type: "leave", roomId: this.roomId, userId: this.userId });
|
|
242
123
|
this.ws.close();
|
|
243
124
|
}
|
|
125
|
+
this.peer?.destroy();
|
|
126
|
+
this.localStream?.getTracks().forEach((track) => track.stop());
|
|
127
|
+
this.isConnectedFlag = false;
|
|
244
128
|
this.emit("disconnected");
|
|
245
129
|
}
|
|
246
130
|
toggleMute() {
|
|
@@ -254,26 +138,9 @@ var VoiceClient = class {
|
|
|
254
138
|
isMuted() {
|
|
255
139
|
return this.localStream?.getAudioTracks()[0]?.enabled === false;
|
|
256
140
|
}
|
|
257
|
-
setSpeakingThreshold(threshold) {
|
|
258
|
-
this.speakingThreshold = Math.min(1, Math.max(0, threshold));
|
|
259
|
-
}
|
|
260
|
-
getRemoteStream(userId) {
|
|
261
|
-
if (userId) {
|
|
262
|
-
const stream = this.remoteStreams.get(userId);
|
|
263
|
-
return stream || null;
|
|
264
|
-
}
|
|
265
|
-
const firstStream = this.remoteStreams.values().next().value;
|
|
266
|
-
return firstStream || null;
|
|
267
|
-
}
|
|
268
|
-
getPeers() {
|
|
269
|
-
return Array.from(this.peers.keys());
|
|
270
|
-
}
|
|
271
141
|
getUserId() {
|
|
272
142
|
return this.userId;
|
|
273
143
|
}
|
|
274
|
-
isConnectedToRoom() {
|
|
275
|
-
return this.isConnected;
|
|
276
|
-
}
|
|
277
144
|
};
|
|
278
145
|
export {
|
|
279
146
|
VoiceClient
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voicemaster/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "WebRTC voice communication core library",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -17,10 +17,17 @@
|
|
|
17
17
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
18
18
|
"prepublishOnly": "npm run build"
|
|
19
19
|
},
|
|
20
|
-
"keywords": [
|
|
20
|
+
"keywords": [
|
|
21
|
+
"webrtc",
|
|
22
|
+
"voice",
|
|
23
|
+
"audio",
|
|
24
|
+
"p2p",
|
|
25
|
+
"typescript"
|
|
26
|
+
],
|
|
21
27
|
"author": "Sergey Minasyan",
|
|
22
28
|
"license": "MIT",
|
|
23
29
|
"dependencies": {
|
|
30
|
+
"@voicemaster/core": "^1.0.1",
|
|
24
31
|
"simple-peer": "^9.11.1"
|
|
25
32
|
},
|
|
26
33
|
"devDependencies": {
|
|
@@ -28,4 +35,4 @@
|
|
|
28
35
|
"tsup": "^8.0.0",
|
|
29
36
|
"typescript": "^5.3.0"
|
|
30
37
|
}
|
|
31
|
-
}
|
|
38
|
+
}
|
package/src/VoiceClient.ts
CHANGED
|
@@ -8,52 +8,24 @@ export interface VoiceClientConfig {
|
|
|
8
8
|
iceServers?: RTCIceServer[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export interface VoiceEvents {
|
|
12
|
-
connected: () => void;
|
|
13
|
-
disconnected: () => void;
|
|
14
|
-
remoteStream: (stream: MediaStream) => void;
|
|
15
|
-
localStream: (stream: MediaStream) => void;
|
|
16
|
-
error: (error: Error) => void;
|
|
17
|
-
userJoined: (userId: string) => void;
|
|
18
|
-
userLeft: (userId: string) => void;
|
|
19
|
-
speaking: (userId: string) => void;
|
|
20
|
-
stoppedSpeaking: (userId: string) => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type EventHandler<T extends keyof VoiceEvents> = VoiceEvents[T];
|
|
24
|
-
|
|
25
11
|
export class VoiceClient {
|
|
26
12
|
private ws: WebSocket | null = null;
|
|
27
|
-
private
|
|
13
|
+
private peer: Peer.Instance | null = null;
|
|
28
14
|
private localStream: MediaStream | null = null;
|
|
29
|
-
private
|
|
30
|
-
private eventHandlers:
|
|
31
|
-
private userId: string;
|
|
32
|
-
private roomId: string;
|
|
15
|
+
private remoteStream: MediaStream | null = null;
|
|
16
|
+
private eventHandlers: Map<string, Function[]> = new Map();
|
|
33
17
|
private signalingUrl: string;
|
|
18
|
+
private roomId: string;
|
|
19
|
+
private userId: string;
|
|
34
20
|
private iceServers: RTCIceServer[];
|
|
35
|
-
private
|
|
36
|
-
private audioContext: AudioContext | null = null;
|
|
37
|
-
private sourceNode: MediaStreamAudioSourceNode | null = null;
|
|
38
|
-
private analyserNode: AnalyserNode | null = null;
|
|
39
|
-
private speakingThreshold = 0.05;
|
|
40
|
-
private isSpeaking = false;
|
|
41
|
-
private reconnectAttempts = 0;
|
|
42
|
-
private maxReconnectAttempts = 5;
|
|
43
|
-
private isConnected = false;
|
|
21
|
+
private isConnectedFlag = false;
|
|
44
22
|
|
|
45
23
|
constructor(config: VoiceClientConfig) {
|
|
46
|
-
this.userId = config.userId;
|
|
47
|
-
this.roomId = config.roomId;
|
|
48
24
|
this.signalingUrl = config.signalingUrl;
|
|
25
|
+
this.roomId = config.roomId;
|
|
26
|
+
this.userId = config.userId;
|
|
49
27
|
this.iceServers = config.iceServers || [
|
|
50
|
-
{ urls: 'stun:stun.l.google.com:19302' }
|
|
51
|
-
{ urls: 'stun:stun.relay.metered.ca:80' },
|
|
52
|
-
{
|
|
53
|
-
urls: 'turn:openrelay.metered.ca:80',
|
|
54
|
-
username: 'openrelayproject',
|
|
55
|
-
credential: 'openrelayproject'
|
|
56
|
-
}
|
|
28
|
+
{ urls: 'stun:stun.l.google.com:19302' }
|
|
57
29
|
];
|
|
58
30
|
|
|
59
31
|
if (config.autoConnect !== false) {
|
|
@@ -61,25 +33,17 @@ export class VoiceClient {
|
|
|
61
33
|
}
|
|
62
34
|
}
|
|
63
35
|
|
|
64
|
-
on
|
|
65
|
-
if (!this.eventHandlers
|
|
66
|
-
this.eventHandlers
|
|
36
|
+
on(event: string, callback: Function): void {
|
|
37
|
+
if (!this.eventHandlers.has(event)) {
|
|
38
|
+
this.eventHandlers.set(event, []);
|
|
67
39
|
}
|
|
68
|
-
this.eventHandlers
|
|
40
|
+
this.eventHandlers.get(event)!.push(callback);
|
|
69
41
|
}
|
|
70
42
|
|
|
71
|
-
|
|
72
|
-
const handlers = this.eventHandlers
|
|
43
|
+
private emit(event: string, ...args: any[]): void {
|
|
44
|
+
const handlers = this.eventHandlers.get(event);
|
|
73
45
|
if (handlers) {
|
|
74
|
-
|
|
75
|
-
if (index !== -1) handlers.splice(index, 1);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private emit<K extends keyof VoiceEvents>(event: K, ...args: Parameters<EventHandler<K>>): void {
|
|
80
|
-
const handlers = this.eventHandlers[event];
|
|
81
|
-
if (handlers) {
|
|
82
|
-
handlers.forEach(handler => handler(...args as any));
|
|
46
|
+
handlers.forEach(handler => handler(...args));
|
|
83
47
|
}
|
|
84
48
|
}
|
|
85
49
|
|
|
@@ -88,8 +52,7 @@ export class VoiceClient {
|
|
|
88
52
|
await this.initMicrophone();
|
|
89
53
|
this.initWebSocket();
|
|
90
54
|
} catch (error) {
|
|
91
|
-
this.emit('error', error
|
|
92
|
-
this.handleReconnect();
|
|
55
|
+
this.emit('error', error);
|
|
93
56
|
}
|
|
94
57
|
}
|
|
95
58
|
|
|
@@ -98,64 +61,10 @@ export class VoiceClient {
|
|
|
98
61
|
audio: {
|
|
99
62
|
echoCancellation: true,
|
|
100
63
|
noiseSuppression: true,
|
|
101
|
-
autoGainControl: true
|
|
102
|
-
sampleRate: 48000,
|
|
103
|
-
channelCount: 1
|
|
64
|
+
autoGainControl: true
|
|
104
65
|
}
|
|
105
66
|
});
|
|
106
|
-
|
|
107
67
|
this.emit('localStream', this.localStream);
|
|
108
|
-
this.setupSpeakingDetection();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private setupSpeakingDetection(): void {
|
|
112
|
-
if (!this.localStream) return;
|
|
113
|
-
|
|
114
|
-
this.audioContext = new AudioContext();
|
|
115
|
-
this.sourceNode = this.audioContext.createMediaStreamSource(this.localStream);
|
|
116
|
-
this.analyserNode = this.audioContext.createAnalyser();
|
|
117
|
-
this.analyserNode.fftSize = 256;
|
|
118
|
-
|
|
119
|
-
this.sourceNode.connect(this.analyserNode);
|
|
120
|
-
this.sourceNode.connect(this.audioContext.destination);
|
|
121
|
-
|
|
122
|
-
const dataArray = new Uint8Array(this.analyserNode.frequencyBinCount);
|
|
123
|
-
|
|
124
|
-
this.speakingDetectionInterval = setInterval(() => {
|
|
125
|
-
if (!this.analyserNode) return;
|
|
126
|
-
|
|
127
|
-
this.analyserNode.getByteTimeDomainData(dataArray);
|
|
128
|
-
let maxSample = 0;
|
|
129
|
-
|
|
130
|
-
for (let i = 0; i < dataArray.length; i++) {
|
|
131
|
-
const sample = Math.abs(dataArray[i] / 128 - 1);
|
|
132
|
-
if (sample > maxSample) maxSample = sample;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const isCurrentlySpeaking = maxSample > this.speakingThreshold;
|
|
136
|
-
|
|
137
|
-
if (isCurrentlySpeaking && !this.isSpeaking) {
|
|
138
|
-
this.isSpeaking = true;
|
|
139
|
-
this.broadcastSpeakingStatus(true);
|
|
140
|
-
this.emit('speaking', this.userId);
|
|
141
|
-
} else if (!isCurrentlySpeaking && this.isSpeaking) {
|
|
142
|
-
this.isSpeaking = false;
|
|
143
|
-
this.broadcastSpeakingStatus(false);
|
|
144
|
-
this.emit('stoppedSpeaking', this.userId);
|
|
145
|
-
}
|
|
146
|
-
}, 100);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private broadcastSpeakingStatus(isSpeaking: boolean): void {
|
|
150
|
-
this.peers.forEach((peer) => {
|
|
151
|
-
if (peer && peer.connected) {
|
|
152
|
-
peer.send(JSON.stringify({
|
|
153
|
-
type: 'speaking',
|
|
154
|
-
userId: this.userId,
|
|
155
|
-
isSpeaking
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
68
|
}
|
|
160
69
|
|
|
161
70
|
private initWebSocket(): void {
|
|
@@ -163,13 +72,7 @@ export class VoiceClient {
|
|
|
163
72
|
this.ws = new WebSocket(url);
|
|
164
73
|
|
|
165
74
|
this.ws.onopen = () => {
|
|
166
|
-
this.
|
|
167
|
-
this.isConnected = true;
|
|
168
|
-
this.send({
|
|
169
|
-
type: 'join',
|
|
170
|
-
roomId: this.roomId,
|
|
171
|
-
userId: this.userId
|
|
172
|
-
});
|
|
75
|
+
this.send({ type: 'join', roomId: this.roomId, userId: this.userId });
|
|
173
76
|
};
|
|
174
77
|
|
|
175
78
|
this.ws.onmessage = (event) => {
|
|
@@ -178,112 +81,66 @@ export class VoiceClient {
|
|
|
178
81
|
};
|
|
179
82
|
|
|
180
83
|
this.ws.onclose = () => {
|
|
181
|
-
this.isConnected = false;
|
|
182
|
-
this.handleReconnect();
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
this.ws.onerror = (error) => {
|
|
186
|
-
this.emit('error', new Error('WebSocket error'));
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private handleReconnect(): void {
|
|
191
|
-
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
192
|
-
this.reconnectAttempts++;
|
|
193
|
-
setTimeout(() => {
|
|
194
|
-
this.initWebSocket();
|
|
195
|
-
}, 1000 * Math.min(30, this.reconnectAttempts));
|
|
196
|
-
} else {
|
|
197
84
|
this.emit('disconnected');
|
|
198
|
-
}
|
|
85
|
+
};
|
|
199
86
|
}
|
|
200
87
|
|
|
201
88
|
private handleSignalingMessage(message: any): void {
|
|
202
89
|
switch (message.type) {
|
|
203
90
|
case 'user-joined':
|
|
204
|
-
|
|
205
|
-
|
|
91
|
+
if (message.userId !== this.userId) {
|
|
92
|
+
this.initPeer(true);
|
|
93
|
+
this.emit('userJoined', message.userId);
|
|
94
|
+
}
|
|
206
95
|
break;
|
|
207
|
-
|
|
208
96
|
case 'signal':
|
|
209
97
|
this.handlePeerSignal(message.userId, message.payload);
|
|
210
98
|
break;
|
|
211
|
-
|
|
212
99
|
case 'user-left':
|
|
213
|
-
this.closePeer(message.userId);
|
|
214
100
|
this.emit('userLeft', message.userId);
|
|
215
101
|
break;
|
|
216
102
|
}
|
|
217
103
|
}
|
|
218
104
|
|
|
219
|
-
private initPeer(
|
|
220
|
-
if (this.
|
|
105
|
+
private initPeer(initiator: boolean): void {
|
|
106
|
+
if (this.peer) return;
|
|
221
107
|
|
|
222
|
-
|
|
108
|
+
this.peer = new Peer({
|
|
223
109
|
initiator,
|
|
224
110
|
trickle: true,
|
|
225
|
-
stream: this.localStream
|
|
111
|
+
stream: this.localStream || undefined,
|
|
226
112
|
config: { iceServers: this.iceServers }
|
|
227
113
|
});
|
|
228
114
|
|
|
229
|
-
peer.on('signal', (data) => {
|
|
115
|
+
this.peer.on('signal', (data) => {
|
|
230
116
|
this.send({
|
|
231
117
|
type: 'signal',
|
|
232
|
-
userId:
|
|
118
|
+
userId: this.userId,
|
|
119
|
+
roomId: this.roomId,
|
|
233
120
|
payload: data
|
|
234
121
|
});
|
|
235
122
|
});
|
|
236
123
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
peer.on('data', (data) => {
|
|
243
|
-
try {
|
|
244
|
-
const parsed = JSON.parse(data.toString());
|
|
245
|
-
if (parsed.type === 'speaking') {
|
|
246
|
-
if (parsed.isSpeaking) {
|
|
247
|
-
this.emit('speaking', parsed.userId);
|
|
248
|
-
} else {
|
|
249
|
-
this.emit('stoppedSpeaking', parsed.userId);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
} catch (e) {
|
|
253
|
-
// Binary data, ignore
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
peer.on('error', (err) => {
|
|
258
|
-
this.emit('error', err);
|
|
259
|
-
});
|
|
124
|
+
this.peer.on('stream', (stream) => {
|
|
125
|
+
this.remoteStream = stream;
|
|
126
|
+
this.emit('remoteStream', stream);
|
|
127
|
+
});
|
|
260
128
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
129
|
+
this.peer.on('connect', () => {
|
|
130
|
+
this.isConnectedFlag = true;
|
|
131
|
+
this.emit('connected');
|
|
132
|
+
});
|
|
264
133
|
|
|
265
|
-
|
|
266
|
-
|
|
134
|
+
this.peer.on('error', (err) => {
|
|
135
|
+
console.error('Peer error:', err);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
267
138
|
|
|
268
139
|
private handlePeerSignal(targetUserId: string, signal: any): void {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (!peer) {
|
|
272
|
-
this.initPeer(targetUserId, false);
|
|
273
|
-
peer = this.peers.get(targetUserId);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (peer) {
|
|
277
|
-
peer.signal(signal);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private closePeer(userId: string): void {
|
|
282
|
-
const peer = this.peers.get(userId);
|
|
283
|
-
if (peer) {
|
|
284
|
-
peer.destroy();
|
|
285
|
-
this.peers.delete(userId);
|
|
140
|
+
if (!this.peer) {
|
|
141
|
+
this.initPeer(false);
|
|
286
142
|
}
|
|
143
|
+
this.peer?.signal(signal);
|
|
287
144
|
}
|
|
288
145
|
|
|
289
146
|
private send(data: any): void {
|
|
@@ -293,27 +150,13 @@ export class VoiceClient {
|
|
|
293
150
|
}
|
|
294
151
|
|
|
295
152
|
disconnect(): void {
|
|
296
|
-
if (this.speakingDetectionInterval) {
|
|
297
|
-
clearInterval(this.speakingDetectionInterval);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (this.audioContext) {
|
|
301
|
-
this.audioContext.close();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
this.peers.forEach((peer) => {
|
|
305
|
-
peer.destroy();
|
|
306
|
-
});
|
|
307
|
-
this.peers.clear();
|
|
308
|
-
|
|
309
|
-
if (this.localStream) {
|
|
310
|
-
this.localStream.getTracks().forEach(track => track.stop());
|
|
311
|
-
}
|
|
312
|
-
|
|
313
153
|
if (this.ws) {
|
|
154
|
+
this.send({ type: 'leave', roomId: this.roomId, userId: this.userId });
|
|
314
155
|
this.ws.close();
|
|
315
156
|
}
|
|
316
|
-
|
|
157
|
+
this.peer?.destroy();
|
|
158
|
+
this.localStream?.getTracks().forEach(track => track.stop());
|
|
159
|
+
this.isConnectedFlag = false;
|
|
317
160
|
this.emit('disconnected');
|
|
318
161
|
}
|
|
319
162
|
|
|
@@ -330,30 +173,19 @@ export class VoiceClient {
|
|
|
330
173
|
return this.localStream?.getAudioTracks()[0]?.enabled === false;
|
|
331
174
|
}
|
|
332
175
|
|
|
333
|
-
setSpeakingThreshold(threshold: number): void {
|
|
334
|
-
this.speakingThreshold = Math.min(1, Math.max(0, threshold));
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
getRemoteStream(userId?: string): MediaStream | null {
|
|
338
|
-
if (userId) {
|
|
339
|
-
const stream = this.remoteStreams.get(userId);
|
|
340
|
-
return stream || null;
|
|
341
|
-
}
|
|
342
|
-
const firstStream = this.remoteStreams.values().next().value;
|
|
343
|
-
return firstStream || null;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
getPeers(): string[] {
|
|
347
|
-
return Array.from(this.peers.keys());
|
|
348
|
-
}
|
|
349
|
-
|
|
350
176
|
getUserId(): string {
|
|
351
177
|
return this.userId;
|
|
352
178
|
}
|
|
353
|
-
|
|
354
|
-
isConnectedToRoom(): boolean {
|
|
355
|
-
return this.isConnected;
|
|
356
|
-
}
|
|
357
179
|
}
|
|
358
180
|
|
|
359
|
-
export
|
|
181
|
+
export interface VoiceEvents {
|
|
182
|
+
connected: () => void;
|
|
183
|
+
disconnected: () => void;
|
|
184
|
+
remoteStream: (stream: MediaStream) => void;
|
|
185
|
+
localStream: (stream: MediaStream) => void;
|
|
186
|
+
error: (error: Error) => void;
|
|
187
|
+
userJoined: (userId: string) => void;
|
|
188
|
+
userLeft: (userId: string) => void;
|
|
189
|
+
speaking: (userId: string) => void;
|
|
190
|
+
stoppedSpeaking: (userId: string) => void;
|
|
191
|
+
}
|