@tetrax/voice-sdk 1.0.0

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.
Files changed (3) hide show
  1. package/README.md +117 -0
  2. package/package.json +11 -0
  3. package/src/index.js +273 -0
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @tetrax/voice-sdk
2
+
3
+ Client SDK for TETRAX Communication Platform as a Service (CPaaS). Connects seamlessly to TETRAX Voice real-time audio signaling and SFU servers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @tetrax/voice-sdk
9
+ ```
10
+
11
+ Or reference locally via your application's `package.json`:
12
+
13
+ ```json
14
+ "dependencies": {
15
+ "@tetrax/voice-sdk": "file:../path/to/tetrax-sdk"
16
+ }
17
+ ```
18
+
19
+ ## Basic Usage
20
+
21
+ ```javascript
22
+ import { TetraxVoice } from '@tetrax/voice-sdk';
23
+
24
+ // Initialize SDK
25
+ const client = new TetraxVoice({
26
+ apiKey: 'trx_live_your_key',
27
+ apiUri: 'https://api.tetrax.io' // Optional: defaults to production TETRAX cloud
28
+ });
29
+
30
+ // Register Event Listeners
31
+ client.on('track', ({ stream, userId }) => {
32
+ // Play remote audio
33
+ const audio = new Audio();
34
+ audio.srcObject = stream;
35
+ audio.play();
36
+ });
37
+
38
+ client.on('user-joined', ({ userId }) => {
39
+ console.log(`User ${userId} has joined the voice call`);
40
+ });
41
+
42
+ client.on('user-left', ({ userId }) => {
43
+ console.log(`User ${userId} has left the voice call`);
44
+ });
45
+
46
+ // Connect to signaling and join the call room
47
+ await client.connect(voiceToken);
48
+ await client.joinRoom('room_123');
49
+
50
+ // Toggle microphone
51
+ await client.mute();
52
+ await client.unmute();
53
+
54
+ // Leave room
55
+ await client.leaveRoom();
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Hosting & Distribution Options
61
+
62
+ Because an SDK (Software Development Kit) is a client-side library rather than a running process, **it does not need server hosting** like a backend api or database does.
63
+
64
+ Instead of running on a server, it needs to be hosted on a **package registry** or **version control host** so that target front-end applications can install it.
65
+
66
+ Here are the best ways to host and distribute the **`@[tetrax-sdk]`** package:
67
+
68
+ ---
69
+
70
+ ### Option 1: Publish to the NPM Registry (Standard Production Approach)
71
+ This makes the SDK available via a simple `npm install @tetrax/voice-sdk` (like other packages such as `react` or `socket.io-client`).
72
+ * **Where it is hosted:** [npmjs.com](https://www.npmjs.com/) (The global NPM registry).
73
+ * **Cost:** Free for public packages; paid for private packages.
74
+ * **How to distribute:**
75
+ 1. Register an NPM account (or create an organization named `tetrax`).
76
+ 2. Log in via your terminal inside the `tetrax-sdk` folder:
77
+ ```bash
78
+ npm login
79
+ ```
80
+ 3. Publish the package:
81
+ ```bash
82
+ npm publish --access public
83
+ ```
84
+
85
+ ---
86
+
87
+ ### Option 2: Host on GitHub / Git Repository (Best for Private/Internal Use)
88
+ If you want to keep the SDK private or share it easily without publishing to public NPM, you can host it in its own Git repository.
89
+ * **Where it is hosted:** GitHub, GitLab, or Bitbucket.
90
+ * **How to install:** Other projects can install it directly from the Git URL:
91
+ ```bash
92
+ npm install git+https://github.com/your-username/tetrax-voice-sdk.git
93
+ ```
94
+ Or add it directly to `package.json`:
95
+ ```json
96
+ "dependencies": {
97
+ "@tetrax/voice-sdk": "git+https://github.com/your-username/tetrax-voice-sdk.git"
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ### Option 3: Monorepo / Local Workspace (Best for single-repository setups)
104
+ If you are developing both the backend/frontend client and the SDK together, you can keep them in the same repository.
105
+ * **Where it is hosted:** Included in your main project repository.
106
+ * **How to install:** Keep using the local link in your `package.json`:
107
+ ```json
108
+ "dependencies": {
109
+ "@tetrax/voice-sdk": "file:../tetrax-sdk"
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ### Summary
116
+ * **Mediasoup/Signaling Backend:** Must be hosted on a server (like VPS, AWS, DigitalOcean) with raw ports open for WebRTC connections.
117
+ * **Tetrax SDK:** Hosted on **NPM** or **GitHub** as a library of code that gets bundled directly into your client's web browser builds.
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@tetrax/voice-sdk",
3
+ "version": "1.0.0",
4
+ "description": "TETRAX Voice Client SDK for Mediasoup Real-Time Audio Rooms",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "dependencies": {
8
+ "mediasoup-client": "^3.20.0",
9
+ "socket.io-client": "^4.8.3"
10
+ }
11
+ }
package/src/index.js ADDED
@@ -0,0 +1,273 @@
1
+ import { io } from 'socket.io-client';
2
+ import { Device } from 'mediasoup-client';
3
+
4
+ class EventEmitter {
5
+ constructor() {
6
+ this.listeners = {};
7
+ }
8
+
9
+ on(event, cb) {
10
+ if (!this.listeners[event]) this.listeners[event] = [];
11
+ this.listeners[event].push(cb);
12
+ }
13
+
14
+ off(event, cb) {
15
+ if (!this.listeners[event]) return;
16
+ this.listeners[event] = this.listeners[event].filter(l => l !== cb);
17
+ }
18
+
19
+ emit(event, ...args) {
20
+ if (!this.listeners[event]) return;
21
+ this.listeners[event].forEach(cb => cb(...args));
22
+ }
23
+ }
24
+
25
+ export class TetraxVoice extends EventEmitter {
26
+ constructor({ apiKey, apiUri }) {
27
+ super();
28
+ this.apiKey = apiKey;
29
+ this.apiUri = apiUri;
30
+ this.socket = null;
31
+ this.device = null;
32
+ this.roomId = null;
33
+ this.localStream = null;
34
+
35
+ // Transports and Media references
36
+ this.sendTransport = null;
37
+ this.recvTransport = null;
38
+ this.audioProducer = null;
39
+ this.consumers = new Map();
40
+ }
41
+
42
+ /**
43
+ * Connects to the TETRAX signaling server using the token
44
+ */
45
+ async connect(token) {
46
+ return new Promise((resolve, reject) => {
47
+ this.socket = io(this.apiUri, {
48
+ auth: { token }
49
+ });
50
+
51
+ this.socket.on('connect', () => {
52
+ this.emit('connected');
53
+ resolve();
54
+ });
55
+
56
+ this.socket.on('connect_error', (error) => {
57
+ this.emit('error', error);
58
+ reject(error);
59
+ });
60
+
61
+ this.socket.on('new-producer', async ({ producerId, userId }) => {
62
+ await this._consumeProducer(producerId, userId);
63
+ });
64
+
65
+ this.socket.on('user-joined', ({ userId }) => {
66
+ this.emit('user-joined', { userId });
67
+ });
68
+
69
+ this.socket.on('user-left', ({ userId }) => {
70
+ this.emit('user-left', { userId });
71
+ });
72
+
73
+ this.socket.on('user-muted', ({ userId }) => {
74
+ this.emit('user-muted', { userId });
75
+ });
76
+
77
+ this.socket.on('user-unmuted', ({ userId }) => {
78
+ this.emit('user-unmuted', { userId });
79
+ });
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Joins a specific audio room and sets up sending/receiving audio
85
+ */
86
+ async joinRoom(roomId) {
87
+ this.roomId = roomId;
88
+
89
+ return new Promise((resolve, reject) => {
90
+ this.socket.emit('join-room', { roomId }, async (res) => {
91
+ if (res?.error) {
92
+ return reject(new Error(res.error));
93
+ }
94
+
95
+ try {
96
+ // 1. Initialize Mediasoup Device
97
+ this.device = new Device();
98
+ await this.device.load({ routerRtpCapabilities: res.rtpCapabilities });
99
+
100
+ // 2. Setup Transports and Media
101
+ await this._setupSendTransport(roomId);
102
+ await this._setupRecvTransport(roomId, res.existingProducers || []);
103
+
104
+ this.emit('joined', res);
105
+ resolve(res);
106
+ } catch (err) {
107
+ reject(err);
108
+ }
109
+ });
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Mute the microphone stream
115
+ */
116
+ async mute() {
117
+ if (this.audioProducer) {
118
+ await this.audioProducer.pause();
119
+ this.socket.emit('mute', { roomId: this.roomId });
120
+ this.emit('local-mute-changed', true);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Unmute the microphone stream
126
+ */
127
+ async unmute() {
128
+ if (this.audioProducer) {
129
+ await this.audioProducer.resume();
130
+ this.socket.emit('unmute', { roomId: this.roomId });
131
+ this.emit('local-mute-changed', false);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Leave current room and clean up media and signaling state
137
+ */
138
+ async leaveRoom() {
139
+ if (this.roomId && this.socket) {
140
+ this.socket.emit('leave-room', { roomId: this.roomId });
141
+ }
142
+
143
+ if (this.localStream) {
144
+ this.localStream.getTracks().forEach(track => track.stop());
145
+ this.localStream = null;
146
+ }
147
+
148
+ if (this.sendTransport) {
149
+ try { this.sendTransport.close(); } catch (_) {}
150
+ this.sendTransport = null;
151
+ }
152
+
153
+ if (this.recvTransport) {
154
+ try { this.recvTransport.close(); } catch (_) {}
155
+ this.recvTransport = null;
156
+ }
157
+
158
+ if (this.audioProducer) {
159
+ try { this.audioProducer.close(); } catch (_) {}
160
+ this.audioProducer = null;
161
+ }
162
+
163
+ this.consumers.forEach(consumer => {
164
+ try { consumer.close(); } catch (_) {}
165
+ });
166
+ this.consumers.clear();
167
+ this.device = null;
168
+ this.roomId = null;
169
+ }
170
+
171
+ /**
172
+ * Disconnect entirely from signaling server
173
+ */
174
+ disconnect() {
175
+ this.leaveRoom();
176
+ if (this.socket) {
177
+ this.socket.disconnect();
178
+ this.socket = null;
179
+ }
180
+ }
181
+
182
+ // --- Internal WebRTC Helpers ---
183
+
184
+ async _setupSendTransport(roomId) {
185
+ return new Promise((resolve) => {
186
+ this.socket.emit('create-transport', { roomId, direction: 'send' }, async (sendRes) => {
187
+ if (sendRes?.error) throw new Error(sendRes.error);
188
+
189
+ this.sendTransport = this.device.createSendTransport(sendRes.params);
190
+
191
+ this.sendTransport.on('connect', ({ dtlsParameters }, callback, errback) => {
192
+ this.socket.emit('connect-transport', { roomId, transportId: this.sendTransport.id, dtlsParameters }, (connRes) => {
193
+ if (connRes?.error) errback(connRes.error);
194
+ else callback();
195
+ });
196
+ });
197
+
198
+ this.sendTransport.on('produce', ({ kind, rtpParameters }, callback, errback) => {
199
+ this.socket.emit('produce', { roomId, transportId: this.sendTransport.id, kind, rtpParameters }, (prodRes) => {
200
+ if (prodRes?.error) errback(prodRes.error);
201
+ else callback({ id: prodRes.id });
202
+ });
203
+ });
204
+
205
+ // Initialize Local Capture
206
+ try {
207
+ this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
208
+ const track = this.localStream.getAudioTracks()[0];
209
+ if (track) {
210
+ this.audioProducer = await this.sendTransport.produce({ track });
211
+ }
212
+ } catch (err) {
213
+ this.emit('warning', 'Microphone access denied or unavailable. Joined in listen-only mode.');
214
+ }
215
+
216
+ resolve();
217
+ });
218
+ });
219
+ }
220
+
221
+ async _setupRecvTransport(roomId, existingProducers) {
222
+ return new Promise((resolve) => {
223
+ this.socket.emit('create-transport', { roomId, direction: 'recv' }, async (recvRes) => {
224
+ if (recvRes?.error) throw new Error(recvRes.error);
225
+
226
+ this.recvTransport = this.device.createRecvTransport(recvRes.params);
227
+
228
+ this.recvTransport.on('connect', ({ dtlsParameters }, callback, errback) => {
229
+ this.socket.emit('connect-transport', { roomId, transportId: this.recvTransport.id, dtlsParameters }, (connRes) => {
230
+ if (connRes?.error) errback(connRes.error);
231
+ else callback();
232
+ });
233
+ });
234
+
235
+ // Consume all initial users
236
+ for (const p of existingProducers) {
237
+ await this._consumeProducer(p.producerId, p.userId);
238
+ }
239
+
240
+ resolve();
241
+ });
242
+ });
243
+ }
244
+
245
+ async _consumeProducer(producerId, userId) {
246
+ if (!this.device || !this.recvTransport) return;
247
+
248
+ this.socket.emit('consume', {
249
+ roomId: this.roomId,
250
+ transportId: this.recvTransport.id,
251
+ producerId,
252
+ rtpCapabilities: this.device.rtpCapabilities
253
+ }, async (res) => {
254
+ if (res?.error) return;
255
+
256
+ const consumer = await this.recvTransport.consume({
257
+ id: res.id,
258
+ producerId: res.producerId,
259
+ kind: res.kind,
260
+ rtpParameters: res.rtpParameters
261
+ });
262
+
263
+ this.consumers.set(consumer.id, consumer);
264
+
265
+ this.socket.emit('resume-consumer', { roomId: this.roomId, consumerId: consumer.id }, (resumedRes) => {
266
+ if (resumedRes?.error) return;
267
+
268
+ const stream = new MediaStream([consumer.track]);
269
+ this.emit('track', { track: consumer.track, stream, userId, consumerId: consumer.id });
270
+ });
271
+ });
272
+ }
273
+ }