@voicemaster/core 1.0.5 → 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 CHANGED
@@ -34,16 +34,5 @@ declare class VoiceClient {
34
34
  isMuted(): boolean;
35
35
  getUserId(): string;
36
36
  }
37
- interface VoiceEvents {
38
- connected: () => void;
39
- disconnected: () => void;
40
- remoteStream: (stream: MediaStream) => void;
41
- localStream: (stream: MediaStream) => void;
42
- error: (error: Error) => void;
43
- userJoined: (userId: string) => void;
44
- userLeft: (userId: string) => void;
45
- speaking: (userId: string) => void;
46
- stoppedSpeaking: (userId: string) => void;
47
- }
48
37
 
49
- export { VoiceClient, type VoiceClientConfig, type VoiceEvents };
38
+ export { VoiceClient, type VoiceClientConfig };
package/dist/index.d.ts CHANGED
@@ -34,16 +34,5 @@ declare class VoiceClient {
34
34
  isMuted(): boolean;
35
35
  getUserId(): string;
36
36
  }
37
- interface VoiceEvents {
38
- connected: () => void;
39
- disconnected: () => void;
40
- remoteStream: (stream: MediaStream) => void;
41
- localStream: (stream: MediaStream) => void;
42
- error: (error: Error) => void;
43
- userJoined: (userId: string) => void;
44
- userLeft: (userId: string) => void;
45
- speaking: (userId: string) => void;
46
- stoppedSpeaking: (userId: string) => void;
47
- }
48
37
 
49
- export { VoiceClient, type VoiceClientConfig, type VoiceEvents };
38
+ export { VoiceClient, type VoiceClientConfig };
package/dist/index.js CHANGED
@@ -95,6 +95,16 @@ var VoiceClient = class {
95
95
  });
96
96
  }
97
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
+ };
98
108
  }
99
109
  initWebSocket() {
100
110
  const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
@@ -155,8 +165,10 @@ var VoiceClient = class {
155
165
  await this.pc.setRemoteDescription(answer);
156
166
  }
157
167
  async handleCandidate(message) {
158
- const candidate = new RTCIceCandidate(message.candidate);
159
- await this.pc.addIceCandidate(candidate);
168
+ if (message.candidate) {
169
+ const candidate = new RTCIceCandidate(message.candidate);
170
+ await this.pc.addIceCandidate(candidate);
171
+ }
160
172
  }
161
173
  send(data) {
162
174
  if (this.ws?.readyState === WebSocket.OPEN) {
@@ -169,7 +181,9 @@ var VoiceClient = class {
169
181
  this.ws.close();
170
182
  }
171
183
  this.pc?.close();
172
- this.localStream?.getTracks().forEach((track) => track.stop());
184
+ if (this.localStream) {
185
+ this.localStream.getTracks().forEach((track) => track.stop());
186
+ }
173
187
  this.isConnectedFlag = false;
174
188
  this.emit("disconnected");
175
189
  }
package/dist/index.mjs CHANGED
@@ -69,6 +69,16 @@ var VoiceClient = class {
69
69
  });
70
70
  }
71
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
+ };
72
82
  }
73
83
  initWebSocket() {
74
84
  const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
@@ -129,8 +139,10 @@ var VoiceClient = class {
129
139
  await this.pc.setRemoteDescription(answer);
130
140
  }
131
141
  async handleCandidate(message) {
132
- const candidate = new RTCIceCandidate(message.candidate);
133
- await this.pc.addIceCandidate(candidate);
142
+ if (message.candidate) {
143
+ const candidate = new RTCIceCandidate(message.candidate);
144
+ await this.pc.addIceCandidate(candidate);
145
+ }
134
146
  }
135
147
  send(data) {
136
148
  if (this.ws?.readyState === WebSocket.OPEN) {
@@ -143,7 +155,9 @@ var VoiceClient = class {
143
155
  this.ws.close();
144
156
  }
145
157
  this.pc?.close();
146
- this.localStream?.getTracks().forEach((track) => track.stop());
158
+ if (this.localStream) {
159
+ this.localStream.getTracks().forEach((track) => track.stop());
160
+ }
147
161
  this.isConnectedFlag = false;
148
162
  this.emit("disconnected");
149
163
  }
package/package.json CHANGED
@@ -1,38 +1,37 @@
1
- {
2
- "name": "@voicemaster/core",
3
- "version": "1.0.5",
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
- "@voicemaster/core": "^1.0.1",
31
- "simple-peer": "^9.11.1"
32
- },
33
- "devDependencies": {
34
- "@types/simple-peer": "^9.11.8",
35
- "tsup": "^8.0.0",
36
- "typescript": "^5.3.0"
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
+ }
@@ -1,210 +1,214 @@
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
-
95
- private initWebSocket(): void {
96
- const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
97
- this.ws = new WebSocket(url);
98
-
99
- this.ws.onopen = () => {
100
- this.send({ type: 'join', roomId: this.roomId, userId: this.userId });
101
- };
102
-
103
- this.ws.onmessage = async (event) => {
104
- const message = JSON.parse(event.data);
105
- await this.handleSignalingMessage(message);
106
- };
107
-
108
- this.ws.onclose = () => {
109
- this.emit('disconnected');
110
- };
111
- }
112
-
113
- private async handleSignalingMessage(message: any): Promise<void> {
114
- switch (message.type) {
115
- case 'user-joined':
116
- if (message.userId !== this.userId) {
117
- await this.createOffer();
118
- this.emit('userJoined', message.userId);
119
- }
120
- break;
121
- case 'offer':
122
- await this.handleOffer(message);
123
- break;
124
- case 'answer':
125
- await this.handleAnswer(message);
126
- break;
127
- case 'candidate':
128
- await this.handleCandidate(message);
129
- break;
130
- case 'user-left':
131
- this.emit('userLeft', message.userId);
132
- break;
133
- }
134
- }
135
-
136
- private async createOffer(): Promise<void> {
137
- const offer = await this.pc!.createOffer();
138
- await this.pc!.setLocalDescription(offer);
139
- this.send({
140
- type: 'offer',
141
- sdp: this.pc!.localDescription
142
- });
143
- }
144
-
145
- private async handleOffer(message: any): Promise<void> {
146
- const offer = new RTCSessionDescription(message.sdp);
147
- await this.pc!.setRemoteDescription(offer);
148
- const answer = await this.pc!.createAnswer();
149
- await this.pc!.setLocalDescription(answer);
150
- this.send({
151
- type: 'answer',
152
- sdp: this.pc!.localDescription
153
- });
154
- }
155
-
156
- private async handleAnswer(message: any): Promise<void> {
157
- const answer = new RTCSessionDescription(message.sdp);
158
- await this.pc!.setRemoteDescription(answer);
159
- }
160
-
161
- private async handleCandidate(message: any): Promise<void> {
162
- const candidate = new RTCIceCandidate(message.candidate);
163
- await this.pc!.addIceCandidate(candidate);
164
- }
165
-
166
- private send(data: any): void {
167
- if (this.ws?.readyState === WebSocket.OPEN) {
168
- this.ws.send(JSON.stringify(data));
169
- }
170
- }
171
-
172
- disconnect(): void {
173
- if (this.ws) {
174
- this.send({ type: 'leave', roomId: this.roomId, userId: this.userId });
175
- this.ws.close();
176
- }
177
- this.pc?.close();
178
- this.localStream?.getTracks().forEach(track => track.stop());
179
- this.isConnectedFlag = false;
180
- this.emit('disconnected');
181
- }
182
-
183
- toggleMute(): void {
184
- if (this.localStream) {
185
- const audioTrack = this.localStream.getAudioTracks()[0];
186
- if (audioTrack) {
187
- audioTrack.enabled = !audioTrack.enabled;
188
- }
189
- }
190
- }
191
-
192
- isMuted(): boolean {
193
- return this.localStream?.getAudioTracks()[0]?.enabled === false;
194
- }
195
-
196
- getUserId(): string {
197
- return this.userId;
198
- }
199
- }
200
- export interface VoiceEvents {
201
- connected: () => void;
202
- disconnected: () => void;
203
- remoteStream: (stream: MediaStream) => void;
204
- localStream: (stream: MediaStream) => void;
205
- error: (error: Error) => void;
206
- userJoined: (userId: string) => void;
207
- userLeft: (userId: string) => void;
208
- speaking: (userId: string) => void;
209
- stoppedSpeaking: (userId: string) => void;
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
+ }
210
214
  }
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { VoiceClient } from './VoiceClient';
2
- export type { VoiceClientConfig, VoiceEvents } from './VoiceClient';
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
  }