@voicemaster/core 1.0.4 → 1.0.6
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 +7 -15
- package/dist/index.d.ts +7 -15
- package/dist/index.js +71 -59
- package/dist/index.mjs +71 -49
- package/package.json +37 -38
- package/src/VoiceClient.ts +213 -202
- package/src/index.ts +2 -2
- package/tsconfig.json +15 -15
package/dist/index.d.mts
CHANGED
|
@@ -7,7 +7,7 @@ interface VoiceClientConfig {
|
|
|
7
7
|
}
|
|
8
8
|
declare class VoiceClient {
|
|
9
9
|
private ws;
|
|
10
|
-
private
|
|
10
|
+
private pc;
|
|
11
11
|
private localStream;
|
|
12
12
|
private remoteStream;
|
|
13
13
|
private eventHandlers;
|
|
@@ -21,26 +21,18 @@ declare class VoiceClient {
|
|
|
21
21
|
private emit;
|
|
22
22
|
connect(): Promise<void>;
|
|
23
23
|
private initMicrophone;
|
|
24
|
+
private initPeerConnection;
|
|
24
25
|
private initWebSocket;
|
|
25
26
|
private handleSignalingMessage;
|
|
26
|
-
private
|
|
27
|
-
private
|
|
27
|
+
private createOffer;
|
|
28
|
+
private handleOffer;
|
|
29
|
+
private handleAnswer;
|
|
30
|
+
private handleCandidate;
|
|
28
31
|
private send;
|
|
29
32
|
disconnect(): void;
|
|
30
33
|
toggleMute(): void;
|
|
31
34
|
isMuted(): boolean;
|
|
32
35
|
getUserId(): string;
|
|
33
36
|
}
|
|
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;
|
|
44
|
-
}
|
|
45
37
|
|
|
46
|
-
export { VoiceClient, type VoiceClientConfig
|
|
38
|
+
export { VoiceClient, type VoiceClientConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ interface VoiceClientConfig {
|
|
|
7
7
|
}
|
|
8
8
|
declare class VoiceClient {
|
|
9
9
|
private ws;
|
|
10
|
-
private
|
|
10
|
+
private pc;
|
|
11
11
|
private localStream;
|
|
12
12
|
private remoteStream;
|
|
13
13
|
private eventHandlers;
|
|
@@ -21,26 +21,18 @@ declare class VoiceClient {
|
|
|
21
21
|
private emit;
|
|
22
22
|
connect(): Promise<void>;
|
|
23
23
|
private initMicrophone;
|
|
24
|
+
private initPeerConnection;
|
|
24
25
|
private initWebSocket;
|
|
25
26
|
private handleSignalingMessage;
|
|
26
|
-
private
|
|
27
|
-
private
|
|
27
|
+
private createOffer;
|
|
28
|
+
private handleOffer;
|
|
29
|
+
private handleAnswer;
|
|
30
|
+
private handleCandidate;
|
|
28
31
|
private send;
|
|
29
32
|
disconnect(): void;
|
|
30
33
|
toggleMute(): void;
|
|
31
34
|
isMuted(): boolean;
|
|
32
35
|
getUserId(): string;
|
|
33
36
|
}
|
|
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;
|
|
44
|
-
}
|
|
45
37
|
|
|
46
|
-
export { VoiceClient, type VoiceClientConfig
|
|
38
|
+
export { VoiceClient, type VoiceClientConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
@@ -35,11 +25,10 @@ __export(index_exports, {
|
|
|
35
25
|
module.exports = __toCommonJS(index_exports);
|
|
36
26
|
|
|
37
27
|
// src/VoiceClient.ts
|
|
38
|
-
var import_simple_peer = __toESM(require("simple-peer"));
|
|
39
28
|
var VoiceClient = class {
|
|
40
29
|
constructor(config) {
|
|
41
30
|
this.ws = null;
|
|
42
|
-
this.
|
|
31
|
+
this.pc = null;
|
|
43
32
|
this.localStream = null;
|
|
44
33
|
this.remoteStream = null;
|
|
45
34
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
@@ -48,8 +37,7 @@ var VoiceClient = class {
|
|
|
48
37
|
this.roomId = config.roomId;
|
|
49
38
|
this.userId = config.userId;
|
|
50
39
|
this.iceServers = config.iceServers || [
|
|
51
|
-
{ urls: "stun:stun.l.google.com:19302" }
|
|
52
|
-
{ urls: "stun:stun1.l.google.com:19302" }
|
|
40
|
+
{ urls: "stun:stun.l.google.com:19302" }
|
|
53
41
|
];
|
|
54
42
|
if (config.autoConnect !== false) {
|
|
55
43
|
this.connect();
|
|
@@ -70,6 +58,7 @@ var VoiceClient = class {
|
|
|
70
58
|
async connect() {
|
|
71
59
|
try {
|
|
72
60
|
await this.initMicrophone();
|
|
61
|
+
this.initPeerConnection();
|
|
73
62
|
this.initWebSocket();
|
|
74
63
|
} catch (error) {
|
|
75
64
|
this.emit("error", error);
|
|
@@ -85,78 +74,101 @@ var VoiceClient = class {
|
|
|
85
74
|
});
|
|
86
75
|
this.emit("localStream", this.localStream);
|
|
87
76
|
}
|
|
77
|
+
initPeerConnection() {
|
|
78
|
+
this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
|
79
|
+
this.localStream?.getTracks().forEach((track) => {
|
|
80
|
+
this.pc.addTrack(track, this.localStream);
|
|
81
|
+
});
|
|
82
|
+
this.pc.ontrack = (event) => {
|
|
83
|
+
this.remoteStream = event.streams[0];
|
|
84
|
+
this.emit("remoteStream", this.remoteStream);
|
|
85
|
+
if (!this.isConnectedFlag) {
|
|
86
|
+
this.isConnectedFlag = true;
|
|
87
|
+
this.emit("connected");
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
this.pc.onicecandidate = (event) => {
|
|
91
|
+
if (event.candidate) {
|
|
92
|
+
this.send({
|
|
93
|
+
type: "candidate",
|
|
94
|
+
candidate: event.candidate
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
this.pc.onconnectionstatechange = () => {
|
|
99
|
+
if (this.pc?.connectionState === "connected") {
|
|
100
|
+
if (!this.isConnectedFlag) {
|
|
101
|
+
this.isConnectedFlag = true;
|
|
102
|
+
this.emit("connected");
|
|
103
|
+
}
|
|
104
|
+
} else if (this.pc?.connectionState === "disconnected") {
|
|
105
|
+
this.emit("disconnected");
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
88
109
|
initWebSocket() {
|
|
89
110
|
const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
|
|
90
111
|
this.ws = new WebSocket(url);
|
|
91
112
|
this.ws.onopen = () => {
|
|
92
113
|
this.send({ type: "join", roomId: this.roomId, userId: this.userId });
|
|
93
114
|
};
|
|
94
|
-
this.ws.onmessage = (event) => {
|
|
115
|
+
this.ws.onmessage = async (event) => {
|
|
95
116
|
const message = JSON.parse(event.data);
|
|
96
|
-
this.handleSignalingMessage(message);
|
|
117
|
+
await this.handleSignalingMessage(message);
|
|
97
118
|
};
|
|
98
119
|
this.ws.onclose = () => {
|
|
99
120
|
this.emit("disconnected");
|
|
100
121
|
};
|
|
101
122
|
}
|
|
102
|
-
handleSignalingMessage(message) {
|
|
123
|
+
async handleSignalingMessage(message) {
|
|
103
124
|
switch (message.type) {
|
|
104
125
|
case "user-joined":
|
|
105
126
|
if (message.userId !== this.userId) {
|
|
106
|
-
this.
|
|
127
|
+
await this.createOffer();
|
|
107
128
|
this.emit("userJoined", message.userId);
|
|
108
129
|
}
|
|
109
130
|
break;
|
|
110
|
-
case "
|
|
111
|
-
this.
|
|
131
|
+
case "offer":
|
|
132
|
+
await this.handleOffer(message);
|
|
133
|
+
break;
|
|
134
|
+
case "answer":
|
|
135
|
+
await this.handleAnswer(message);
|
|
136
|
+
break;
|
|
137
|
+
case "candidate":
|
|
138
|
+
await this.handleCandidate(message);
|
|
112
139
|
break;
|
|
113
140
|
case "user-left":
|
|
114
141
|
this.emit("userLeft", message.userId);
|
|
115
142
|
break;
|
|
116
143
|
}
|
|
117
144
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
initiator,
|
|
125
|
-
trickle: true,
|
|
126
|
-
stream: this.localStream || void 0,
|
|
127
|
-
config
|
|
128
|
-
});
|
|
129
|
-
this.peer.on("signal", (data) => {
|
|
130
|
-
this.send({
|
|
131
|
-
type: "signal",
|
|
132
|
-
userId: this.userId,
|
|
133
|
-
roomId: this.roomId,
|
|
134
|
-
payload: data
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
this.peer.on("stream", (stream) => {
|
|
138
|
-
this.remoteStream = stream;
|
|
139
|
-
this.emit("remoteStream", stream);
|
|
140
|
-
if (!this.isConnectedFlag) {
|
|
141
|
-
this.isConnectedFlag = true;
|
|
142
|
-
this.emit("connected");
|
|
143
|
-
}
|
|
145
|
+
async createOffer() {
|
|
146
|
+
const offer = await this.pc.createOffer();
|
|
147
|
+
await this.pc.setLocalDescription(offer);
|
|
148
|
+
this.send({
|
|
149
|
+
type: "offer",
|
|
150
|
+
sdp: this.pc.localDescription
|
|
144
151
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.
|
|
152
|
-
|
|
152
|
+
}
|
|
153
|
+
async handleOffer(message) {
|
|
154
|
+
const offer = new RTCSessionDescription(message.sdp);
|
|
155
|
+
await this.pc.setRemoteDescription(offer);
|
|
156
|
+
const answer = await this.pc.createAnswer();
|
|
157
|
+
await this.pc.setLocalDescription(answer);
|
|
158
|
+
this.send({
|
|
159
|
+
type: "answer",
|
|
160
|
+
sdp: this.pc.localDescription
|
|
153
161
|
});
|
|
154
162
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
async handleAnswer(message) {
|
|
164
|
+
const answer = new RTCSessionDescription(message.sdp);
|
|
165
|
+
await this.pc.setRemoteDescription(answer);
|
|
166
|
+
}
|
|
167
|
+
async handleCandidate(message) {
|
|
168
|
+
if (message.candidate) {
|
|
169
|
+
const candidate = new RTCIceCandidate(message.candidate);
|
|
170
|
+
await this.pc.addIceCandidate(candidate);
|
|
158
171
|
}
|
|
159
|
-
this.peer?.signal(signal);
|
|
160
172
|
}
|
|
161
173
|
send(data) {
|
|
162
174
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -168,7 +180,7 @@ var VoiceClient = class {
|
|
|
168
180
|
this.send({ type: "leave", roomId: this.roomId, userId: this.userId });
|
|
169
181
|
this.ws.close();
|
|
170
182
|
}
|
|
171
|
-
this.
|
|
183
|
+
this.pc?.close();
|
|
172
184
|
if (this.localStream) {
|
|
173
185
|
this.localStream.getTracks().forEach((track) => track.stop());
|
|
174
186
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// src/VoiceClient.ts
|
|
2
|
-
import Peer from "simple-peer";
|
|
3
2
|
var VoiceClient = class {
|
|
4
3
|
constructor(config) {
|
|
5
4
|
this.ws = null;
|
|
6
|
-
this.
|
|
5
|
+
this.pc = null;
|
|
7
6
|
this.localStream = null;
|
|
8
7
|
this.remoteStream = null;
|
|
9
8
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
@@ -12,8 +11,7 @@ var VoiceClient = class {
|
|
|
12
11
|
this.roomId = config.roomId;
|
|
13
12
|
this.userId = config.userId;
|
|
14
13
|
this.iceServers = config.iceServers || [
|
|
15
|
-
{ urls: "stun:stun.l.google.com:19302" }
|
|
16
|
-
{ urls: "stun:stun1.l.google.com:19302" }
|
|
14
|
+
{ urls: "stun:stun.l.google.com:19302" }
|
|
17
15
|
];
|
|
18
16
|
if (config.autoConnect !== false) {
|
|
19
17
|
this.connect();
|
|
@@ -34,6 +32,7 @@ var VoiceClient = class {
|
|
|
34
32
|
async connect() {
|
|
35
33
|
try {
|
|
36
34
|
await this.initMicrophone();
|
|
35
|
+
this.initPeerConnection();
|
|
37
36
|
this.initWebSocket();
|
|
38
37
|
} catch (error) {
|
|
39
38
|
this.emit("error", error);
|
|
@@ -49,78 +48,101 @@ var VoiceClient = class {
|
|
|
49
48
|
});
|
|
50
49
|
this.emit("localStream", this.localStream);
|
|
51
50
|
}
|
|
51
|
+
initPeerConnection() {
|
|
52
|
+
this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
|
53
|
+
this.localStream?.getTracks().forEach((track) => {
|
|
54
|
+
this.pc.addTrack(track, this.localStream);
|
|
55
|
+
});
|
|
56
|
+
this.pc.ontrack = (event) => {
|
|
57
|
+
this.remoteStream = event.streams[0];
|
|
58
|
+
this.emit("remoteStream", this.remoteStream);
|
|
59
|
+
if (!this.isConnectedFlag) {
|
|
60
|
+
this.isConnectedFlag = true;
|
|
61
|
+
this.emit("connected");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
this.pc.onicecandidate = (event) => {
|
|
65
|
+
if (event.candidate) {
|
|
66
|
+
this.send({
|
|
67
|
+
type: "candidate",
|
|
68
|
+
candidate: event.candidate
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
this.pc.onconnectionstatechange = () => {
|
|
73
|
+
if (this.pc?.connectionState === "connected") {
|
|
74
|
+
if (!this.isConnectedFlag) {
|
|
75
|
+
this.isConnectedFlag = true;
|
|
76
|
+
this.emit("connected");
|
|
77
|
+
}
|
|
78
|
+
} else if (this.pc?.connectionState === "disconnected") {
|
|
79
|
+
this.emit("disconnected");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
52
83
|
initWebSocket() {
|
|
53
84
|
const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
|
|
54
85
|
this.ws = new WebSocket(url);
|
|
55
86
|
this.ws.onopen = () => {
|
|
56
87
|
this.send({ type: "join", roomId: this.roomId, userId: this.userId });
|
|
57
88
|
};
|
|
58
|
-
this.ws.onmessage = (event) => {
|
|
89
|
+
this.ws.onmessage = async (event) => {
|
|
59
90
|
const message = JSON.parse(event.data);
|
|
60
|
-
this.handleSignalingMessage(message);
|
|
91
|
+
await this.handleSignalingMessage(message);
|
|
61
92
|
};
|
|
62
93
|
this.ws.onclose = () => {
|
|
63
94
|
this.emit("disconnected");
|
|
64
95
|
};
|
|
65
96
|
}
|
|
66
|
-
handleSignalingMessage(message) {
|
|
97
|
+
async handleSignalingMessage(message) {
|
|
67
98
|
switch (message.type) {
|
|
68
99
|
case "user-joined":
|
|
69
100
|
if (message.userId !== this.userId) {
|
|
70
|
-
this.
|
|
101
|
+
await this.createOffer();
|
|
71
102
|
this.emit("userJoined", message.userId);
|
|
72
103
|
}
|
|
73
104
|
break;
|
|
74
|
-
case "
|
|
75
|
-
this.
|
|
105
|
+
case "offer":
|
|
106
|
+
await this.handleOffer(message);
|
|
107
|
+
break;
|
|
108
|
+
case "answer":
|
|
109
|
+
await this.handleAnswer(message);
|
|
110
|
+
break;
|
|
111
|
+
case "candidate":
|
|
112
|
+
await this.handleCandidate(message);
|
|
76
113
|
break;
|
|
77
114
|
case "user-left":
|
|
78
115
|
this.emit("userLeft", message.userId);
|
|
79
116
|
break;
|
|
80
117
|
}
|
|
81
118
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
initiator,
|
|
89
|
-
trickle: true,
|
|
90
|
-
stream: this.localStream || void 0,
|
|
91
|
-
config
|
|
92
|
-
});
|
|
93
|
-
this.peer.on("signal", (data) => {
|
|
94
|
-
this.send({
|
|
95
|
-
type: "signal",
|
|
96
|
-
userId: this.userId,
|
|
97
|
-
roomId: this.roomId,
|
|
98
|
-
payload: data
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
this.peer.on("stream", (stream) => {
|
|
102
|
-
this.remoteStream = stream;
|
|
103
|
-
this.emit("remoteStream", stream);
|
|
104
|
-
if (!this.isConnectedFlag) {
|
|
105
|
-
this.isConnectedFlag = true;
|
|
106
|
-
this.emit("connected");
|
|
107
|
-
}
|
|
119
|
+
async createOffer() {
|
|
120
|
+
const offer = await this.pc.createOffer();
|
|
121
|
+
await this.pc.setLocalDescription(offer);
|
|
122
|
+
this.send({
|
|
123
|
+
type: "offer",
|
|
124
|
+
sdp: this.pc.localDescription
|
|
108
125
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
|
|
126
|
+
}
|
|
127
|
+
async handleOffer(message) {
|
|
128
|
+
const offer = new RTCSessionDescription(message.sdp);
|
|
129
|
+
await this.pc.setRemoteDescription(offer);
|
|
130
|
+
const answer = await this.pc.createAnswer();
|
|
131
|
+
await this.pc.setLocalDescription(answer);
|
|
132
|
+
this.send({
|
|
133
|
+
type: "answer",
|
|
134
|
+
sdp: this.pc.localDescription
|
|
117
135
|
});
|
|
118
136
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
137
|
+
async handleAnswer(message) {
|
|
138
|
+
const answer = new RTCSessionDescription(message.sdp);
|
|
139
|
+
await this.pc.setRemoteDescription(answer);
|
|
140
|
+
}
|
|
141
|
+
async handleCandidate(message) {
|
|
142
|
+
if (message.candidate) {
|
|
143
|
+
const candidate = new RTCIceCandidate(message.candidate);
|
|
144
|
+
await this.pc.addIceCandidate(candidate);
|
|
122
145
|
}
|
|
123
|
-
this.peer?.signal(signal);
|
|
124
146
|
}
|
|
125
147
|
send(data) {
|
|
126
148
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -132,7 +154,7 @@ var VoiceClient = class {
|
|
|
132
154
|
this.send({ type: "leave", roomId: this.roomId, userId: this.userId });
|
|
133
155
|
this.ws.close();
|
|
134
156
|
}
|
|
135
|
-
this.
|
|
157
|
+
this.pc?.close();
|
|
136
158
|
if (this.localStream) {
|
|
137
159
|
this.localStream.getTracks().forEach((track) => track.stop());
|
|
138
160
|
}
|
package/package.json
CHANGED
|
@@ -1,38 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@voicemaster/core",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "WebRTC voice communication core library",
|
|
5
|
-
"main": "./dist/index.js",
|
|
6
|
-
"module": "./dist/index.mjs",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.mjs",
|
|
12
|
-
"require": "./dist/index.js"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
17
|
-
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
18
|
-
"prepublishOnly": "npm run build"
|
|
19
|
-
},
|
|
20
|
-
"keywords": [
|
|
21
|
-
"webrtc",
|
|
22
|
-
"voice",
|
|
23
|
-
"audio",
|
|
24
|
-
"p2p",
|
|
25
|
-
"typescript"
|
|
26
|
-
],
|
|
27
|
-
"author": "Sergey Minasyan",
|
|
28
|
-
"license": "MIT",
|
|
29
|
-
"dependencies": {
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@voicemaster/core",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "WebRTC voice communication core library",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
17
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"webrtc",
|
|
22
|
+
"voice",
|
|
23
|
+
"audio",
|
|
24
|
+
"p2p",
|
|
25
|
+
"typescript"
|
|
26
|
+
],
|
|
27
|
+
"author": "Sergey Minasyan",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"simple-peer": "^9.11.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/simple-peer": "^9.11.8",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5.3.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/VoiceClient.ts
CHANGED
|
@@ -1,203 +1,214 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
private
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
private
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.emit('
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
this.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
|
|
21
|
+
constructor(config: VoiceClientConfig) {
|
|
22
|
+
this.signalingUrl = config.signalingUrl;
|
|
23
|
+
this.roomId = config.roomId;
|
|
24
|
+
this.userId = config.userId;
|
|
25
|
+
this.iceServers = config.iceServers || [
|
|
26
|
+
{ urls: 'stun:stun.l.google.com:19302' }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
if (config.autoConnect !== false) {
|
|
30
|
+
this.connect();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
on(event: string, callback: Function): void {
|
|
35
|
+
if (!this.eventHandlers.has(event)) {
|
|
36
|
+
this.eventHandlers.set(event, []);
|
|
37
|
+
}
|
|
38
|
+
this.eventHandlers.get(event)!.push(callback);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private emit(event: string, ...args: any[]): void {
|
|
42
|
+
const handlers = this.eventHandlers.get(event);
|
|
43
|
+
if (handlers) {
|
|
44
|
+
handlers.forEach(handler => handler(...args));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async connect(): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
await this.initMicrophone();
|
|
51
|
+
this.initPeerConnection();
|
|
52
|
+
this.initWebSocket();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this.emit('error', error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async initMicrophone(): Promise<void> {
|
|
59
|
+
this.localStream = await navigator.mediaDevices.getUserMedia({
|
|
60
|
+
audio: {
|
|
61
|
+
echoCancellation: true,
|
|
62
|
+
noiseSuppression: true,
|
|
63
|
+
autoGainControl: true
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
this.emit('localStream', this.localStream);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private initPeerConnection(): void {
|
|
70
|
+
this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
|
71
|
+
|
|
72
|
+
this.localStream?.getTracks().forEach(track => {
|
|
73
|
+
this.pc!.addTrack(track, this.localStream!);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.pc.ontrack = (event) => {
|
|
77
|
+
this.remoteStream = event.streams[0];
|
|
78
|
+
this.emit('remoteStream', this.remoteStream);
|
|
79
|
+
if (!this.isConnectedFlag) {
|
|
80
|
+
this.isConnectedFlag = true;
|
|
81
|
+
this.emit('connected');
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
this.pc.onicecandidate = (event) => {
|
|
86
|
+
if (event.candidate) {
|
|
87
|
+
this.send({
|
|
88
|
+
type: 'candidate',
|
|
89
|
+
candidate: event.candidate
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
this.pc.onconnectionstatechange = () => {
|
|
95
|
+
if (this.pc?.connectionState === 'connected') {
|
|
96
|
+
if (!this.isConnectedFlag) {
|
|
97
|
+
this.isConnectedFlag = true;
|
|
98
|
+
this.emit('connected');
|
|
99
|
+
}
|
|
100
|
+
} else if (this.pc?.connectionState === 'disconnected') {
|
|
101
|
+
this.emit('disconnected');
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private initWebSocket(): void {
|
|
107
|
+
const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
|
|
108
|
+
this.ws = new WebSocket(url);
|
|
109
|
+
|
|
110
|
+
this.ws.onopen = () => {
|
|
111
|
+
this.send({ type: 'join', roomId: this.roomId, userId: this.userId });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
this.ws.onmessage = async (event) => {
|
|
115
|
+
const message = JSON.parse(event.data);
|
|
116
|
+
await this.handleSignalingMessage(message);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
this.ws.onclose = () => {
|
|
120
|
+
this.emit('disconnected');
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async handleSignalingMessage(message: any): Promise<void> {
|
|
125
|
+
switch (message.type) {
|
|
126
|
+
case 'user-joined':
|
|
127
|
+
if (message.userId !== this.userId) {
|
|
128
|
+
await this.createOffer();
|
|
129
|
+
this.emit('userJoined', message.userId);
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
case 'offer':
|
|
133
|
+
await this.handleOffer(message);
|
|
134
|
+
break;
|
|
135
|
+
case 'answer':
|
|
136
|
+
await this.handleAnswer(message);
|
|
137
|
+
break;
|
|
138
|
+
case 'candidate':
|
|
139
|
+
await this.handleCandidate(message);
|
|
140
|
+
break;
|
|
141
|
+
case 'user-left':
|
|
142
|
+
this.emit('userLeft', message.userId);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private async createOffer(): Promise<void> {
|
|
148
|
+
const offer = await this.pc!.createOffer();
|
|
149
|
+
await this.pc!.setLocalDescription(offer);
|
|
150
|
+
this.send({
|
|
151
|
+
type: 'offer',
|
|
152
|
+
sdp: this.pc!.localDescription
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async handleOffer(message: any): Promise<void> {
|
|
157
|
+
const offer = new RTCSessionDescription(message.sdp);
|
|
158
|
+
await this.pc!.setRemoteDescription(offer);
|
|
159
|
+
const answer = await this.pc!.createAnswer();
|
|
160
|
+
await this.pc!.setLocalDescription(answer);
|
|
161
|
+
this.send({
|
|
162
|
+
type: 'answer',
|
|
163
|
+
sdp: this.pc!.localDescription
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async handleAnswer(message: any): Promise<void> {
|
|
168
|
+
const answer = new RTCSessionDescription(message.sdp);
|
|
169
|
+
await this.pc!.setRemoteDescription(answer);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async handleCandidate(message: any): Promise<void> {
|
|
173
|
+
if (message.candidate) {
|
|
174
|
+
const candidate = new RTCIceCandidate(message.candidate);
|
|
175
|
+
await this.pc!.addIceCandidate(candidate);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private send(data: any): void {
|
|
180
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
181
|
+
this.ws.send(JSON.stringify(data));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
disconnect(): void {
|
|
186
|
+
if (this.ws) {
|
|
187
|
+
this.send({ type: 'leave', roomId: this.roomId, userId: this.userId });
|
|
188
|
+
this.ws.close();
|
|
189
|
+
}
|
|
190
|
+
this.pc?.close();
|
|
191
|
+
if (this.localStream) {
|
|
192
|
+
this.localStream.getTracks().forEach(track => track.stop());
|
|
193
|
+
}
|
|
194
|
+
this.isConnectedFlag = false;
|
|
195
|
+
this.emit('disconnected');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
toggleMute(): void {
|
|
199
|
+
if (this.localStream) {
|
|
200
|
+
const audioTrack = this.localStream.getAudioTracks()[0];
|
|
201
|
+
if (audioTrack) {
|
|
202
|
+
audioTrack.enabled = !audioTrack.enabled;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
isMuted(): boolean {
|
|
208
|
+
return this.localStream?.getAudioTracks()[0]?.enabled === false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getUserId(): string {
|
|
212
|
+
return this.userId;
|
|
213
|
+
}
|
|
203
214
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { VoiceClient } from './VoiceClient';
|
|
2
|
-
export type { VoiceClientConfig
|
|
1
|
+
export { VoiceClient } from './VoiceClient';
|
|
2
|
+
export type { VoiceClientConfig } from './VoiceClient';
|
package/tsconfig.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
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"]
|
|
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
16
|
}
|