@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 CHANGED
@@ -7,7 +7,7 @@ interface VoiceClientConfig {
7
7
  }
8
8
  declare class VoiceClient {
9
9
  private ws;
10
- private peer;
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 initPeer;
27
- private handlePeerSignal;
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, type VoiceEvents };
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 peer;
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 initPeer;
27
- private handlePeerSignal;
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, type VoiceEvents };
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.peer = null;
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.initPeer(true);
127
+ await this.createOffer();
107
128
  this.emit("userJoined", message.userId);
108
129
  }
109
130
  break;
110
- case "signal":
111
- this.handlePeerSignal(message.userId, message.payload);
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
- initPeer(initiator) {
119
- if (this.peer) return;
120
- const config = {
121
- iceServers: this.iceServers
122
- };
123
- this.peer = new import_simple_peer.default({
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
- this.peer.on("connect", () => {
146
- if (!this.isConnectedFlag) {
147
- this.isConnectedFlag = true;
148
- this.emit("connected");
149
- }
150
- });
151
- this.peer.on("error", (err) => {
152
- console.error("Peer error:", err);
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
- handlePeerSignal(targetUserId, signal) {
156
- if (!this.peer) {
157
- this.initPeer(false);
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.peer?.destroy();
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.peer = null;
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.initPeer(true);
101
+ await this.createOffer();
71
102
  this.emit("userJoined", message.userId);
72
103
  }
73
104
  break;
74
- case "signal":
75
- this.handlePeerSignal(message.userId, message.payload);
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
- initPeer(initiator) {
83
- if (this.peer) return;
84
- const config = {
85
- iceServers: this.iceServers
86
- };
87
- this.peer = new Peer({
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
- this.peer.on("connect", () => {
110
- if (!this.isConnectedFlag) {
111
- this.isConnectedFlag = true;
112
- this.emit("connected");
113
- }
114
- });
115
- this.peer.on("error", (err) => {
116
- console.error("Peer error:", err);
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
- handlePeerSignal(targetUserId, signal) {
120
- if (!this.peer) {
121
- this.initPeer(false);
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.peer?.destroy();
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",
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,203 +1,214 @@
1
- import Peer from 'simple-peer';
2
-
3
- export interface VoiceClientConfig {
4
- signalingUrl: string;
5
- roomId: string;
6
- userId: string;
7
- autoConnect?: boolean;
8
- iceServers?: RTCIceServer[];
9
- }
10
-
11
- export class VoiceClient {
12
- private ws: WebSocket | null = null;
13
- private peer: Peer.Instance | null = null;
14
- private localStream: MediaStream | null = null;
15
- private remoteStream: MediaStream | null = null;
16
- private eventHandlers: Map<string, Function[]> = new Map();
17
- private signalingUrl: string;
18
- private roomId: string;
19
- private userId: string;
20
- private iceServers: RTCIceServer[];
21
- private isConnectedFlag = false;
22
-
23
- constructor(config: VoiceClientConfig) {
24
- this.signalingUrl = config.signalingUrl;
25
- this.roomId = config.roomId;
26
- this.userId = config.userId;
27
- this.iceServers = config.iceServers || [
28
- { urls: 'stun:stun.l.google.com:19302' },
29
- { urls: 'stun:stun1.l.google.com:19302' }
30
- ];
31
-
32
- if (config.autoConnect !== false) {
33
- this.connect();
34
- }
35
- }
36
-
37
- on(event: string, callback: Function): void {
38
- if (!this.eventHandlers.has(event)) {
39
- this.eventHandlers.set(event, []);
40
- }
41
- this.eventHandlers.get(event)!.push(callback);
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.initWebSocket();
55
- } catch (error) {
56
- this.emit('error', error);
57
- }
58
- }
59
-
60
- private async initMicrophone(): Promise<void> {
61
- this.localStream = await navigator.mediaDevices.getUserMedia({
62
- audio: {
63
- echoCancellation: true,
64
- noiseSuppression: true,
65
- autoGainControl: true
66
- }
67
- });
68
- this.emit('localStream', this.localStream);
69
- }
70
-
71
- private initWebSocket(): void {
72
- const url = `${this.signalingUrl}?userId=${this.userId}&roomId=${this.roomId}`;
73
- this.ws = new WebSocket(url);
74
-
75
- this.ws.onopen = () => {
76
- this.send({ type: 'join', roomId: this.roomId, userId: this.userId });
77
- };
78
-
79
- this.ws.onmessage = (event) => {
80
- const message = JSON.parse(event.data);
81
- this.handleSignalingMessage(message);
82
- };
83
-
84
- this.ws.onclose = () => {
85
- this.emit('disconnected');
86
- };
87
- }
88
-
89
- private handleSignalingMessage(message: any): void {
90
- switch (message.type) {
91
- case 'user-joined':
92
- if (message.userId !== this.userId) {
93
- this.initPeer(true);
94
- this.emit('userJoined', message.userId);
95
- }
96
- break;
97
- case 'signal':
98
- this.handlePeerSignal(message.userId, message.payload);
99
- break;
100
- case 'user-left':
101
- this.emit('userLeft', message.userId);
102
- break;
103
- }
104
- }
105
-
106
- private initPeer(initiator: boolean): void {
107
- if (this.peer) return;
108
-
109
- const config = {
110
- iceServers: this.iceServers
111
- };
112
-
113
- this.peer = new Peer({
114
- initiator,
115
- trickle: true,
116
- stream: this.localStream || undefined,
117
- config: config
118
- });
119
-
120
- this.peer.on('signal', (data) => {
121
- this.send({
122
- type: 'signal',
123
- userId: this.userId,
124
- roomId: this.roomId,
125
- payload: data
126
- });
127
- });
128
-
129
- this.peer.on('stream', (stream) => {
130
- this.remoteStream = stream;
131
- this.emit('remoteStream', stream);
132
- if (!this.isConnectedFlag) {
133
- this.isConnectedFlag = true;
134
- this.emit('connected');
135
- }
136
- });
137
-
138
- this.peer.on('connect', () => {
139
- if (!this.isConnectedFlag) {
140
- this.isConnectedFlag = true;
141
- this.emit('connected');
142
- }
143
- });
144
-
145
- this.peer.on('error', (err) => {
146
- console.error('Peer error:', err);
147
- });
148
- }
149
-
150
- private handlePeerSignal(targetUserId: string, signal: any): void {
151
- if (!this.peer) {
152
- this.initPeer(false);
153
- }
154
- this.peer?.signal(signal);
155
- }
156
-
157
- private send(data: any): void {
158
- if (this.ws?.readyState === WebSocket.OPEN) {
159
- this.ws.send(JSON.stringify(data));
160
- }
161
- }
162
-
163
- disconnect(): void {
164
- if (this.ws) {
165
- this.send({ type: 'leave', roomId: this.roomId, userId: this.userId });
166
- this.ws.close();
167
- }
168
- this.peer?.destroy();
169
- if (this.localStream) {
170
- this.localStream.getTracks().forEach(track => track.stop());
171
- }
172
- this.isConnectedFlag = false;
173
- this.emit('disconnected');
174
- }
175
-
176
- toggleMute(): void {
177
- if (this.localStream) {
178
- const audioTrack = this.localStream.getAudioTracks()[0];
179
- if (audioTrack) {
180
- audioTrack.enabled = !audioTrack.enabled;
181
- }
182
- }
183
- }
184
-
185
- isMuted(): boolean {
186
- return this.localStream?.getAudioTracks()[0]?.enabled === false;
187
- }
188
-
189
- getUserId(): string {
190
- return this.userId;
191
- }
192
- }
193
- export interface VoiceEvents {
194
- connected: () => void;
195
- disconnected: () => void;
196
- remoteStream: (stream: MediaStream) => void;
197
- localStream: (stream: MediaStream) => void;
198
- error: (error: Error) => void;
199
- userJoined: (userId: string) => void;
200
- userLeft: (userId: string) => void;
201
- speaking: (userId: string) => void;
202
- 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
+ }
203
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
  }