@voicemaster/core 1.0.10 → 1.0.12
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 +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +76 -15
- package/dist/index.mjs +76 -15
- package/package.json +7 -1
- package/src/VoiceClient.ts +0 -240
- package/src/index.ts +0 -2
- package/tsconfig.json +0 -16
package/dist/index.d.mts
CHANGED
|
@@ -10,13 +10,17 @@ declare class VoiceClient {
|
|
|
10
10
|
private pc;
|
|
11
11
|
private localStream;
|
|
12
12
|
private remoteStream;
|
|
13
|
+
private audioSender;
|
|
13
14
|
private eventHandlers;
|
|
14
15
|
private signalingUrl;
|
|
15
16
|
private roomId;
|
|
16
17
|
private userId;
|
|
17
18
|
private iceServers;
|
|
18
19
|
private isConnectedFlag;
|
|
20
|
+
private isMutedFlag;
|
|
19
21
|
private targetUserId;
|
|
22
|
+
private connectingPromise;
|
|
23
|
+
private peers;
|
|
20
24
|
constructor(config: VoiceClientConfig);
|
|
21
25
|
on(event: string, callback: Function): void;
|
|
22
26
|
private emit;
|
|
@@ -31,6 +35,7 @@ declare class VoiceClient {
|
|
|
31
35
|
private handleCandidate;
|
|
32
36
|
private send;
|
|
33
37
|
disconnect(): void;
|
|
38
|
+
getPeers(): string[];
|
|
34
39
|
toggleMute(): void;
|
|
35
40
|
isMuted(): boolean;
|
|
36
41
|
getUserId(): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -10,13 +10,17 @@ declare class VoiceClient {
|
|
|
10
10
|
private pc;
|
|
11
11
|
private localStream;
|
|
12
12
|
private remoteStream;
|
|
13
|
+
private audioSender;
|
|
13
14
|
private eventHandlers;
|
|
14
15
|
private signalingUrl;
|
|
15
16
|
private roomId;
|
|
16
17
|
private userId;
|
|
17
18
|
private iceServers;
|
|
18
19
|
private isConnectedFlag;
|
|
20
|
+
private isMutedFlag;
|
|
19
21
|
private targetUserId;
|
|
22
|
+
private connectingPromise;
|
|
23
|
+
private peers;
|
|
20
24
|
constructor(config: VoiceClientConfig);
|
|
21
25
|
on(event: string, callback: Function): void;
|
|
22
26
|
private emit;
|
|
@@ -31,6 +35,7 @@ declare class VoiceClient {
|
|
|
31
35
|
private handleCandidate;
|
|
32
36
|
private send;
|
|
33
37
|
disconnect(): void;
|
|
38
|
+
getPeers(): string[];
|
|
34
39
|
toggleMute(): void;
|
|
35
40
|
isMuted(): boolean;
|
|
36
41
|
getUserId(): string;
|
package/dist/index.js
CHANGED
|
@@ -31,9 +31,13 @@ var VoiceClient = class {
|
|
|
31
31
|
this.pc = null;
|
|
32
32
|
this.localStream = null;
|
|
33
33
|
this.remoteStream = null;
|
|
34
|
+
this.audioSender = null;
|
|
34
35
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
35
36
|
this.isConnectedFlag = false;
|
|
37
|
+
this.isMutedFlag = false;
|
|
36
38
|
this.targetUserId = null;
|
|
39
|
+
this.connectingPromise = null;
|
|
40
|
+
this.peers = /* @__PURE__ */ new Set();
|
|
37
41
|
this.signalingUrl = config.signalingUrl;
|
|
38
42
|
this.roomId = config.roomId;
|
|
39
43
|
this.userId = config.userId;
|
|
@@ -41,7 +45,7 @@ var VoiceClient = class {
|
|
|
41
45
|
{ urls: "stun:stun.l.google.com:19302" }
|
|
42
46
|
];
|
|
43
47
|
if (config.autoConnect !== false) {
|
|
44
|
-
this.connect();
|
|
48
|
+
Promise.resolve().then(() => this.connect());
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
on(event, callback) {
|
|
@@ -57,13 +61,22 @@ var VoiceClient = class {
|
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
async connect() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this.initPeerConnection();
|
|
63
|
-
this.initWebSocket();
|
|
64
|
-
} catch (error) {
|
|
65
|
-
this.emit("error", error);
|
|
64
|
+
if (this.connectingPromise) {
|
|
65
|
+
return this.connectingPromise;
|
|
66
66
|
}
|
|
67
|
+
this.connectingPromise = (async () => {
|
|
68
|
+
try {
|
|
69
|
+
await this.initMicrophone();
|
|
70
|
+
this.initPeerConnection();
|
|
71
|
+
this.initWebSocket();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
this.emit("error", error);
|
|
74
|
+
throw error;
|
|
75
|
+
} finally {
|
|
76
|
+
this.connectingPromise = null;
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
return this.connectingPromise;
|
|
67
80
|
}
|
|
68
81
|
async initMicrophone() {
|
|
69
82
|
this.localStream = await navigator.mediaDevices.getUserMedia({
|
|
@@ -78,10 +91,20 @@ var VoiceClient = class {
|
|
|
78
91
|
initPeerConnection() {
|
|
79
92
|
this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
|
80
93
|
this.localStream?.getTracks().forEach((track) => {
|
|
81
|
-
this.pc.addTrack(track, this.localStream);
|
|
94
|
+
const sender = this.pc.addTrack(track, this.localStream);
|
|
95
|
+
if (track.kind === "audio") {
|
|
96
|
+
this.audioSender = sender;
|
|
97
|
+
}
|
|
82
98
|
});
|
|
83
99
|
this.pc.ontrack = (event) => {
|
|
84
|
-
|
|
100
|
+
const stream = event.streams?.[0] || new MediaStream([event.track]);
|
|
101
|
+
this.remoteStream = stream;
|
|
102
|
+
console.log("[VoiceClient] Remote track received from peer", {
|
|
103
|
+
track: event.track,
|
|
104
|
+
stream,
|
|
105
|
+
fromUserId: this.targetUserId,
|
|
106
|
+
myUserId: this.userId
|
|
107
|
+
});
|
|
85
108
|
this.emit("remoteStream", this.remoteStream);
|
|
86
109
|
};
|
|
87
110
|
this.pc.onicecandidate = (event) => {
|
|
@@ -105,13 +128,19 @@ var VoiceClient = class {
|
|
|
105
128
|
const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
|
|
106
129
|
this.ws = new WebSocket(url);
|
|
107
130
|
this.ws.onopen = () => {
|
|
131
|
+
console.log("[VoiceClient] WebSocket connected");
|
|
108
132
|
this.send({ type: "join", roomId: this.roomId, userId: this.userId });
|
|
109
133
|
};
|
|
110
134
|
this.ws.onmessage = async (event) => {
|
|
111
135
|
const message = JSON.parse(event.data);
|
|
112
136
|
await this.handleSignalingMessage(message);
|
|
113
137
|
};
|
|
138
|
+
this.ws.onerror = (event) => {
|
|
139
|
+
console.error("[VoiceClient] WebSocket error", event);
|
|
140
|
+
this.emit("error", new Error("WebSocket error"));
|
|
141
|
+
};
|
|
114
142
|
this.ws.onclose = () => {
|
|
143
|
+
console.log("[VoiceClient] WebSocket closed");
|
|
115
144
|
this.emit("disconnected");
|
|
116
145
|
};
|
|
117
146
|
}
|
|
@@ -121,6 +150,7 @@ var VoiceClient = class {
|
|
|
121
150
|
case "user-joined":
|
|
122
151
|
console.log("[VoiceClient] User joined, myId:", this.userId, "theirId:", message.userId);
|
|
123
152
|
if (message.userId !== this.userId) {
|
|
153
|
+
this.peers.add(message.userId);
|
|
124
154
|
console.log("[VoiceClient] Creating offer...");
|
|
125
155
|
this.targetUserId = message.userId;
|
|
126
156
|
await this.createOffer();
|
|
@@ -143,6 +173,7 @@ var VoiceClient = class {
|
|
|
143
173
|
await this.handleCandidate(message);
|
|
144
174
|
break;
|
|
145
175
|
case "user-left":
|
|
176
|
+
this.peers.delete(message.userId);
|
|
146
177
|
this.emit("userLeft", message.userId);
|
|
147
178
|
break;
|
|
148
179
|
default:
|
|
@@ -193,9 +224,12 @@ var VoiceClient = class {
|
|
|
193
224
|
send(data) {
|
|
194
225
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
195
226
|
this.ws.send(JSON.stringify(data));
|
|
227
|
+
} else {
|
|
228
|
+
console.warn("[VoiceClient] WebSocket is not open, cannot send message", data);
|
|
196
229
|
}
|
|
197
230
|
}
|
|
198
231
|
disconnect() {
|
|
232
|
+
this.peers.clear();
|
|
199
233
|
if (this.ws) {
|
|
200
234
|
this.send({ type: "leave", roomId: this.roomId, userId: this.userId });
|
|
201
235
|
this.ws.close();
|
|
@@ -207,16 +241,43 @@ var VoiceClient = class {
|
|
|
207
241
|
this.isConnectedFlag = false;
|
|
208
242
|
this.emit("disconnected");
|
|
209
243
|
}
|
|
244
|
+
getPeers() {
|
|
245
|
+
return Array.from(this.peers);
|
|
246
|
+
}
|
|
210
247
|
toggleMute() {
|
|
211
|
-
if (this.localStream) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
248
|
+
if (!this.localStream) {
|
|
249
|
+
console.warn("[VoiceClient] No local stream available for muting");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const audioTrack = this.localStream.getAudioTracks()[0];
|
|
253
|
+
if (!audioTrack) {
|
|
254
|
+
console.warn("[VoiceClient] No audio track found for muting");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const shouldMute = audioTrack.enabled;
|
|
258
|
+
audioTrack.enabled = !shouldMute;
|
|
259
|
+
this.isMutedFlag = !shouldMute;
|
|
260
|
+
if (this.audioSender) {
|
|
261
|
+
this.audioSender.replaceTrack(shouldMute ? null : audioTrack).then(() => {
|
|
262
|
+
console.log("[VoiceClient] Audio sender replaced after mute toggle", {
|
|
263
|
+
muted: this.isMutedFlag,
|
|
264
|
+
trackId: audioTrack.id,
|
|
265
|
+
enabled: audioTrack.enabled
|
|
266
|
+
});
|
|
267
|
+
}).catch((err) => {
|
|
268
|
+
console.error("[VoiceClient] Failed to replace audio sender track", err);
|
|
269
|
+
});
|
|
216
270
|
}
|
|
271
|
+
console.log(`[VoiceClient] Mute toggled: now ${this.isMutedFlag ? "muted" : "unmuted"}`, {
|
|
272
|
+
trackId: audioTrack.id,
|
|
273
|
+
enabled: audioTrack.enabled,
|
|
274
|
+
state: audioTrack.readyState
|
|
275
|
+
});
|
|
217
276
|
}
|
|
218
277
|
isMuted() {
|
|
219
|
-
|
|
278
|
+
const muted = this.isMutedFlag;
|
|
279
|
+
console.log("[VoiceClient] isMuted check:", { muted });
|
|
280
|
+
return muted;
|
|
220
281
|
}
|
|
221
282
|
getUserId() {
|
|
222
283
|
return this.userId;
|
package/dist/index.mjs
CHANGED
|
@@ -5,9 +5,13 @@ var VoiceClient = class {
|
|
|
5
5
|
this.pc = null;
|
|
6
6
|
this.localStream = null;
|
|
7
7
|
this.remoteStream = null;
|
|
8
|
+
this.audioSender = null;
|
|
8
9
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
9
10
|
this.isConnectedFlag = false;
|
|
11
|
+
this.isMutedFlag = false;
|
|
10
12
|
this.targetUserId = null;
|
|
13
|
+
this.connectingPromise = null;
|
|
14
|
+
this.peers = /* @__PURE__ */ new Set();
|
|
11
15
|
this.signalingUrl = config.signalingUrl;
|
|
12
16
|
this.roomId = config.roomId;
|
|
13
17
|
this.userId = config.userId;
|
|
@@ -15,7 +19,7 @@ var VoiceClient = class {
|
|
|
15
19
|
{ urls: "stun:stun.l.google.com:19302" }
|
|
16
20
|
];
|
|
17
21
|
if (config.autoConnect !== false) {
|
|
18
|
-
this.connect();
|
|
22
|
+
Promise.resolve().then(() => this.connect());
|
|
19
23
|
}
|
|
20
24
|
}
|
|
21
25
|
on(event, callback) {
|
|
@@ -31,13 +35,22 @@ var VoiceClient = class {
|
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
async connect() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.initPeerConnection();
|
|
37
|
-
this.initWebSocket();
|
|
38
|
-
} catch (error) {
|
|
39
|
-
this.emit("error", error);
|
|
38
|
+
if (this.connectingPromise) {
|
|
39
|
+
return this.connectingPromise;
|
|
40
40
|
}
|
|
41
|
+
this.connectingPromise = (async () => {
|
|
42
|
+
try {
|
|
43
|
+
await this.initMicrophone();
|
|
44
|
+
this.initPeerConnection();
|
|
45
|
+
this.initWebSocket();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.emit("error", error);
|
|
48
|
+
throw error;
|
|
49
|
+
} finally {
|
|
50
|
+
this.connectingPromise = null;
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
return this.connectingPromise;
|
|
41
54
|
}
|
|
42
55
|
async initMicrophone() {
|
|
43
56
|
this.localStream = await navigator.mediaDevices.getUserMedia({
|
|
@@ -52,10 +65,20 @@ var VoiceClient = class {
|
|
|
52
65
|
initPeerConnection() {
|
|
53
66
|
this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
|
54
67
|
this.localStream?.getTracks().forEach((track) => {
|
|
55
|
-
this.pc.addTrack(track, this.localStream);
|
|
68
|
+
const sender = this.pc.addTrack(track, this.localStream);
|
|
69
|
+
if (track.kind === "audio") {
|
|
70
|
+
this.audioSender = sender;
|
|
71
|
+
}
|
|
56
72
|
});
|
|
57
73
|
this.pc.ontrack = (event) => {
|
|
58
|
-
|
|
74
|
+
const stream = event.streams?.[0] || new MediaStream([event.track]);
|
|
75
|
+
this.remoteStream = stream;
|
|
76
|
+
console.log("[VoiceClient] Remote track received from peer", {
|
|
77
|
+
track: event.track,
|
|
78
|
+
stream,
|
|
79
|
+
fromUserId: this.targetUserId,
|
|
80
|
+
myUserId: this.userId
|
|
81
|
+
});
|
|
59
82
|
this.emit("remoteStream", this.remoteStream);
|
|
60
83
|
};
|
|
61
84
|
this.pc.onicecandidate = (event) => {
|
|
@@ -79,13 +102,19 @@ var VoiceClient = class {
|
|
|
79
102
|
const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
|
|
80
103
|
this.ws = new WebSocket(url);
|
|
81
104
|
this.ws.onopen = () => {
|
|
105
|
+
console.log("[VoiceClient] WebSocket connected");
|
|
82
106
|
this.send({ type: "join", roomId: this.roomId, userId: this.userId });
|
|
83
107
|
};
|
|
84
108
|
this.ws.onmessage = async (event) => {
|
|
85
109
|
const message = JSON.parse(event.data);
|
|
86
110
|
await this.handleSignalingMessage(message);
|
|
87
111
|
};
|
|
112
|
+
this.ws.onerror = (event) => {
|
|
113
|
+
console.error("[VoiceClient] WebSocket error", event);
|
|
114
|
+
this.emit("error", new Error("WebSocket error"));
|
|
115
|
+
};
|
|
88
116
|
this.ws.onclose = () => {
|
|
117
|
+
console.log("[VoiceClient] WebSocket closed");
|
|
89
118
|
this.emit("disconnected");
|
|
90
119
|
};
|
|
91
120
|
}
|
|
@@ -95,6 +124,7 @@ var VoiceClient = class {
|
|
|
95
124
|
case "user-joined":
|
|
96
125
|
console.log("[VoiceClient] User joined, myId:", this.userId, "theirId:", message.userId);
|
|
97
126
|
if (message.userId !== this.userId) {
|
|
127
|
+
this.peers.add(message.userId);
|
|
98
128
|
console.log("[VoiceClient] Creating offer...");
|
|
99
129
|
this.targetUserId = message.userId;
|
|
100
130
|
await this.createOffer();
|
|
@@ -117,6 +147,7 @@ var VoiceClient = class {
|
|
|
117
147
|
await this.handleCandidate(message);
|
|
118
148
|
break;
|
|
119
149
|
case "user-left":
|
|
150
|
+
this.peers.delete(message.userId);
|
|
120
151
|
this.emit("userLeft", message.userId);
|
|
121
152
|
break;
|
|
122
153
|
default:
|
|
@@ -167,9 +198,12 @@ var VoiceClient = class {
|
|
|
167
198
|
send(data) {
|
|
168
199
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
169
200
|
this.ws.send(JSON.stringify(data));
|
|
201
|
+
} else {
|
|
202
|
+
console.warn("[VoiceClient] WebSocket is not open, cannot send message", data);
|
|
170
203
|
}
|
|
171
204
|
}
|
|
172
205
|
disconnect() {
|
|
206
|
+
this.peers.clear();
|
|
173
207
|
if (this.ws) {
|
|
174
208
|
this.send({ type: "leave", roomId: this.roomId, userId: this.userId });
|
|
175
209
|
this.ws.close();
|
|
@@ -181,16 +215,43 @@ var VoiceClient = class {
|
|
|
181
215
|
this.isConnectedFlag = false;
|
|
182
216
|
this.emit("disconnected");
|
|
183
217
|
}
|
|
218
|
+
getPeers() {
|
|
219
|
+
return Array.from(this.peers);
|
|
220
|
+
}
|
|
184
221
|
toggleMute() {
|
|
185
|
-
if (this.localStream) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
222
|
+
if (!this.localStream) {
|
|
223
|
+
console.warn("[VoiceClient] No local stream available for muting");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const audioTrack = this.localStream.getAudioTracks()[0];
|
|
227
|
+
if (!audioTrack) {
|
|
228
|
+
console.warn("[VoiceClient] No audio track found for muting");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const shouldMute = audioTrack.enabled;
|
|
232
|
+
audioTrack.enabled = !shouldMute;
|
|
233
|
+
this.isMutedFlag = !shouldMute;
|
|
234
|
+
if (this.audioSender) {
|
|
235
|
+
this.audioSender.replaceTrack(shouldMute ? null : audioTrack).then(() => {
|
|
236
|
+
console.log("[VoiceClient] Audio sender replaced after mute toggle", {
|
|
237
|
+
muted: this.isMutedFlag,
|
|
238
|
+
trackId: audioTrack.id,
|
|
239
|
+
enabled: audioTrack.enabled
|
|
240
|
+
});
|
|
241
|
+
}).catch((err) => {
|
|
242
|
+
console.error("[VoiceClient] Failed to replace audio sender track", err);
|
|
243
|
+
});
|
|
190
244
|
}
|
|
245
|
+
console.log(`[VoiceClient] Mute toggled: now ${this.isMutedFlag ? "muted" : "unmuted"}`, {
|
|
246
|
+
trackId: audioTrack.id,
|
|
247
|
+
enabled: audioTrack.enabled,
|
|
248
|
+
state: audioTrack.readyState
|
|
249
|
+
});
|
|
191
250
|
}
|
|
192
251
|
isMuted() {
|
|
193
|
-
|
|
252
|
+
const muted = this.isMutedFlag;
|
|
253
|
+
console.log("[VoiceClient] isMuted check:", { muted });
|
|
254
|
+
return muted;
|
|
194
255
|
}
|
|
195
256
|
getUserId() {
|
|
196
257
|
return this.userId;
|
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voicemaster/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "WebRTC voice communication core library",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
8
14
|
"exports": {
|
|
9
15
|
".": {
|
|
10
16
|
"types": "./dist/index.d.ts",
|
package/src/VoiceClient.ts
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
export interface VoiceClientConfig {
|
|
2
|
-
signalingUrl: string;
|
|
3
|
-
roomId: string;
|
|
4
|
-
userId: string;
|
|
5
|
-
autoConnect?: boolean;
|
|
6
|
-
iceServers?: RTCIceServer[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class VoiceClient {
|
|
10
|
-
private ws: WebSocket | null = null;
|
|
11
|
-
private pc: RTCPeerConnection | null = null;
|
|
12
|
-
private localStream: MediaStream | null = null;
|
|
13
|
-
private remoteStream: MediaStream | null = null;
|
|
14
|
-
private eventHandlers: Map<string, Function[]> = new Map();
|
|
15
|
-
private signalingUrl: string;
|
|
16
|
-
private roomId: string;
|
|
17
|
-
private userId: string;
|
|
18
|
-
private iceServers: RTCIceServer[];
|
|
19
|
-
private isConnectedFlag = false;
|
|
20
|
-
private targetUserId: string | null = null;
|
|
21
|
-
|
|
22
|
-
constructor(config: VoiceClientConfig) {
|
|
23
|
-
this.signalingUrl = config.signalingUrl;
|
|
24
|
-
this.roomId = config.roomId;
|
|
25
|
-
this.userId = config.userId;
|
|
26
|
-
this.iceServers = config.iceServers || [
|
|
27
|
-
{ urls: 'stun:stun.l.google.com:19302' }
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
if (config.autoConnect !== false) {
|
|
31
|
-
this.connect();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
on(event: string, callback: Function): void {
|
|
36
|
-
if (!this.eventHandlers.has(event)) {
|
|
37
|
-
this.eventHandlers.set(event, []);
|
|
38
|
-
}
|
|
39
|
-
this.eventHandlers.get(event)!.push(callback);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
private emit(event: string, ...args: any[]): void {
|
|
45
|
-
const handlers = this.eventHandlers.get(event);
|
|
46
|
-
if (handlers) {
|
|
47
|
-
handlers.forEach(handler => handler(...args));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async connect(): Promise<void> {
|
|
52
|
-
try {
|
|
53
|
-
await this.initMicrophone();
|
|
54
|
-
this.initPeerConnection();
|
|
55
|
-
this.initWebSocket();
|
|
56
|
-
} catch (error) {
|
|
57
|
-
this.emit('error', error);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private async initMicrophone(): Promise<void> {
|
|
62
|
-
this.localStream = await navigator.mediaDevices.getUserMedia({
|
|
63
|
-
audio: {
|
|
64
|
-
echoCancellation: true,
|
|
65
|
-
noiseSuppression: true,
|
|
66
|
-
autoGainControl: true
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
this.emit('localStream', this.localStream);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private initPeerConnection(): void {
|
|
73
|
-
this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
|
74
|
-
|
|
75
|
-
this.localStream?.getTracks().forEach(track => {
|
|
76
|
-
this.pc!.addTrack(track, this.localStream!);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
this.pc.ontrack = (event) => {
|
|
80
|
-
this.remoteStream = event.streams[0];
|
|
81
|
-
this.emit('remoteStream', this.remoteStream);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
this.pc.onicecandidate = (event) => {
|
|
85
|
-
if (event.candidate) {
|
|
86
|
-
this.send({
|
|
87
|
-
type: 'candidate',
|
|
88
|
-
candidate: event.candidate
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
this.pc.onconnectionstatechange = () => {
|
|
94
|
-
if (this.pc?.connectionState === 'connected') {
|
|
95
|
-
if (!this.isConnectedFlag) {
|
|
96
|
-
this.isConnectedFlag = true;
|
|
97
|
-
this.emit('connected');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private initWebSocket(): void {
|
|
104
|
-
const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
|
|
105
|
-
this.ws = new WebSocket(url);
|
|
106
|
-
|
|
107
|
-
this.ws.onopen = () => {
|
|
108
|
-
this.send({ type: 'join', roomId: this.roomId, userId: this.userId });
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
this.ws.onmessage = async (event) => {
|
|
112
|
-
const message = JSON.parse(event.data);
|
|
113
|
-
await this.handleSignalingMessage(message);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
this.ws.onclose = () => {
|
|
117
|
-
this.emit('disconnected');
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private async handleSignalingMessage(message: any): Promise<void> {
|
|
122
|
-
console.log('[VoiceClient] Received message:', message.type, message);
|
|
123
|
-
|
|
124
|
-
switch (message.type) {
|
|
125
|
-
case 'user-joined':
|
|
126
|
-
console.log('[VoiceClient] User joined, myId:', this.userId, 'theirId:', message.userId);
|
|
127
|
-
if (message.userId !== this.userId) {
|
|
128
|
-
console.log('[VoiceClient] Creating offer...');
|
|
129
|
-
this.targetUserId = message.userId;
|
|
130
|
-
await this.createOffer();
|
|
131
|
-
this.emit('userJoined', message.userId);
|
|
132
|
-
} else {
|
|
133
|
-
console.log('[VoiceClient] Ignoring my own join');
|
|
134
|
-
}
|
|
135
|
-
break;
|
|
136
|
-
case 'offer':
|
|
137
|
-
console.log('[VoiceClient] Received offer');
|
|
138
|
-
this.targetUserId = message.userId;
|
|
139
|
-
await this.handleOffer(message);
|
|
140
|
-
break;
|
|
141
|
-
case 'answer':
|
|
142
|
-
console.log('[VoiceClient] Received answer');
|
|
143
|
-
await this.handleAnswer(message);
|
|
144
|
-
break;
|
|
145
|
-
case 'candidate':
|
|
146
|
-
console.log('[VoiceClient] Received candidate');
|
|
147
|
-
await this.handleCandidate(message);
|
|
148
|
-
break;
|
|
149
|
-
case 'user-left':
|
|
150
|
-
this.emit('userLeft', message.userId);
|
|
151
|
-
break;
|
|
152
|
-
default:
|
|
153
|
-
console.log('[VoiceClient] Unknown message type:', message.type);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private async createOffer(): Promise<void> {
|
|
158
|
-
const offer = await this.pc!.createOffer();
|
|
159
|
-
await this.pc!.setLocalDescription(offer);
|
|
160
|
-
this.send({
|
|
161
|
-
type: 'offer',
|
|
162
|
-
sdp: this.pc!.localDescription
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private async handleOffer(message: any): Promise<void> {
|
|
167
|
-
try {
|
|
168
|
-
console.log('Handling offer', message);
|
|
169
|
-
const offer = new RTCSessionDescription(message.sdp);
|
|
170
|
-
await this.pc!.setRemoteDescription(offer);
|
|
171
|
-
console.log('Remote description set');
|
|
172
|
-
|
|
173
|
-
const answer = await this.pc!.createAnswer();
|
|
174
|
-
console.log('Answer created');
|
|
175
|
-
|
|
176
|
-
await this.pc!.setLocalDescription(answer);
|
|
177
|
-
console.log('Local description set');
|
|
178
|
-
|
|
179
|
-
this.send({
|
|
180
|
-
type: 'answer',
|
|
181
|
-
sdp: this.pc!.localDescription
|
|
182
|
-
});
|
|
183
|
-
} catch (err) {
|
|
184
|
-
console.error('Offer handling error:', err);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private async handleAnswer(message: any): Promise<void> {
|
|
189
|
-
const answer = new RTCSessionDescription(message.sdp);
|
|
190
|
-
await this.pc!.setRemoteDescription(answer);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private async handleCandidate(message: any): Promise<void> {
|
|
194
|
-
try {
|
|
195
|
-
if (message.candidate) {
|
|
196
|
-
const candidate = new RTCIceCandidate(message.candidate);
|
|
197
|
-
await this.pc!.addIceCandidate(candidate);
|
|
198
|
-
console.log('ICE candidate added');
|
|
199
|
-
}
|
|
200
|
-
} catch (err) {
|
|
201
|
-
console.error('Candidate error:', err);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private send(data: any): void {
|
|
206
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
207
|
-
this.ws.send(JSON.stringify(data));
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
disconnect(): void {
|
|
212
|
-
if (this.ws) {
|
|
213
|
-
this.send({ type: 'leave', roomId: this.roomId, userId: this.userId });
|
|
214
|
-
this.ws.close();
|
|
215
|
-
}
|
|
216
|
-
this.pc?.close();
|
|
217
|
-
if (this.localStream) {
|
|
218
|
-
this.localStream.getTracks().forEach(track => track.stop());
|
|
219
|
-
}
|
|
220
|
-
this.isConnectedFlag = false;
|
|
221
|
-
this.emit('disconnected');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
toggleMute(): void {
|
|
225
|
-
if (this.localStream) {
|
|
226
|
-
const audioTrack = this.localStream.getAudioTracks()[0];
|
|
227
|
-
if (audioTrack) {
|
|
228
|
-
audioTrack.enabled = !audioTrack.enabled;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
isMuted(): boolean {
|
|
234
|
-
return this.localStream?.getAudioTracks()[0]?.enabled === false;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
getUserId(): string {
|
|
238
|
-
return this.userId;
|
|
239
|
-
}
|
|
240
|
-
}
|
package/src/index.ts
DELETED
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ES2020", "DOM"],
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"strict": true,
|
|
8
|
-
"esModuleInterop": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"forceConsistentCasingInFileNames": true,
|
|
11
|
-
"resolveJsonModule": true,
|
|
12
|
-
"noEmit": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/**/*"],
|
|
15
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
16
|
-
}
|