@voicemaster/core 1.0.0 → 1.0.1

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 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 peers;
10
+ private peer;
23
11
  private localStream;
24
- private remoteStreams;
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 speakingDetectionInterval;
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<K extends keyof VoiceEvents>(event: K, handler: EventHandler<K>): void;
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
- isConnectedToRoom(): boolean;
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 peers;
10
+ private peer;
23
11
  private localStream;
24
- private remoteStreams;
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 speakingDetectionInterval;
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<K extends keyof VoiceEvents>(event: K, handler: EventHandler<K>): void;
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
- isConnectedToRoom(): boolean;
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.peers = /* @__PURE__ */ new Map();
42
+ this.peer = null;
43
43
  this.localStream = null;
44
- this.remoteStreams = /* @__PURE__ */ new Map();
45
- this.eventHandlers = {};
46
- this.speakingDetectionInterval = null;
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, handler) {
72
- if (!this.eventHandlers[event]) {
73
- this.eventHandlers[event] = [];
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[event];
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.reconnectAttempts = 0;
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
- this.initPeer(message.userId, true);
190
- this.emit("userJoined", message.userId);
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(targetUserId, initiator) {
202
- if (this.peers.has(targetUserId)) return;
203
- const peer = new import_simple_peer.default({
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: targetUserId,
128
+ userId: this.userId,
129
+ roomId: this.roomId,
213
130
  payload: data
214
131
  });
215
132
  });
216
- peer.on("stream", (stream) => {
217
- this.remoteStreams.set(targetUserId, stream);
133
+ this.peer.on("stream", (stream) => {
134
+ this.remoteStream = stream;
218
135
  this.emit("remoteStream", stream);
219
136
  });
220
- peer.on("data", (data) => {
221
- try {
222
- const parsed = JSON.parse(data.toString());
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("close", () => {
237
- this.peers.delete(targetUserId);
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
- let peer = this.peers.get(targetUserId);
243
- if (!peer) {
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.peers = /* @__PURE__ */ new Map();
6
+ this.peer = null;
7
7
  this.localStream = null;
8
- this.remoteStreams = /* @__PURE__ */ new Map();
9
- this.eventHandlers = {};
10
- this.speakingDetectionInterval = null;
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, handler) {
36
- if (!this.eventHandlers[event]) {
37
- this.eventHandlers[event] = [];
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[event];
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.reconnectAttempts = 0;
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
- this.initPeer(message.userId, true);
154
- this.emit("userJoined", message.userId);
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(targetUserId, initiator) {
166
- if (this.peers.has(targetUserId)) return;
167
- const peer = new Peer({
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: targetUserId,
92
+ userId: this.userId,
93
+ roomId: this.roomId,
177
94
  payload: data
178
95
  });
179
96
  });
180
- peer.on("stream", (stream) => {
181
- this.remoteStreams.set(targetUserId, stream);
97
+ this.peer.on("stream", (stream) => {
98
+ this.remoteStream = stream;
182
99
  this.emit("remoteStream", stream);
183
100
  });
184
- peer.on("data", (data) => {
185
- try {
186
- const parsed = JSON.parse(data.toString());
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("close", () => {
201
- this.peers.delete(targetUserId);
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
- let peer = this.peers.get(targetUserId);
207
- if (!peer) {
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.0",
3
+ "version": "1.0.1",
4
4
  "description": "WebRTC voice communication core library",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -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 peers: Map<string, Peer.Instance> = new Map();
13
+ private peer: Peer.Instance | null = null;
28
14
  private localStream: MediaStream | null = null;
29
- private remoteStreams: Map<string, MediaStream> = new Map();
30
- private eventHandlers: Partial<Record<keyof VoiceEvents, Function[]>> = {};
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 speakingDetectionInterval: NodeJS.Timeout | null = null;
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<K extends keyof VoiceEvents>(event: K, handler: EventHandler<K>): void {
65
- if (!this.eventHandlers[event]) {
66
- this.eventHandlers[event] = [];
36
+ on(event: string, callback: Function): void {
37
+ if (!this.eventHandlers.has(event)) {
38
+ this.eventHandlers.set(event, []);
67
39
  }
68
- this.eventHandlers[event]!.push(handler);
40
+ this.eventHandlers.get(event)!.push(callback);
69
41
  }
70
42
 
71
- off<K extends keyof VoiceEvents>(event: K, handler: EventHandler<K>): void {
72
- const handlers = this.eventHandlers[event];
43
+ private emit(event: string, ...args: any[]): void {
44
+ const handlers = this.eventHandlers.get(event);
73
45
  if (handlers) {
74
- const index = handlers.indexOf(handler);
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 as 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.reconnectAttempts = 0;
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
- this.initPeer(message.userId, true);
205
- this.emit('userJoined', message.userId);
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(targetUserId: string, initiator: boolean): void {
220
- if (this.peers.has(targetUserId)) return;
105
+ private initPeer(initiator: boolean): void {
106
+ if (this.peer) return;
221
107
 
222
- const peer = new Peer({
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: targetUserId,
118
+ userId: this.userId,
119
+ roomId: this.roomId,
233
120
  payload: data
234
121
  });
235
122
  });
236
123
 
237
- peer.on('stream', (stream) => {
238
- this.remoteStreams.set(targetUserId, stream);
239
- this.emit('remoteStream', stream);
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
- peer.on('close', () => {
262
- this.peers.delete(targetUserId);
263
- });
129
+ this.peer.on('connect', () => {
130
+ this.isConnectedFlag = true;
131
+ this.emit('connected');
132
+ });
264
133
 
265
- this.peers.set(targetUserId, peer);
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
- let peer = this.peers.get(targetUserId);
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 default VoiceClient;
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
+ }