@fluxrtc/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.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@fluxrtc/sdk",
3
+ "version": "1.0.0",
4
+ "description": "FluxRTC Video SDK — add real-time video and audio to any web app",
5
+ "main": "src/index.js",
6
+ "files": [
7
+ "src/"
8
+ ],
9
+ "scripts": {
10
+ "test": "node -e \"const s = require('./src/index.js'); console.log('SDK exports:', Object.keys(s));\""
11
+ },
12
+ "keywords": [
13
+ "webrtc",
14
+ "video",
15
+ "audio",
16
+ "mediasoup",
17
+ "real-time",
18
+ "conferencing",
19
+ "fluxrtc"
20
+ ],
21
+ "author": "FluxRTC",
22
+ "license": "MIT",
23
+ "peerDependencies": {
24
+ "mediasoup-client": "^3.18.7",
25
+ "socket.io-client": "^4.8.3"
26
+ }
27
+ }
@@ -0,0 +1,304 @@
1
+ const { io } = require('socket.io-client');
2
+ const { Device } = require('mediasoup-client');
3
+
4
+ class VideoClient {
5
+ constructor(signalingUrl) {
6
+ if (!signalingUrl) {
7
+ throw new Error('signalingUrl is required — pass your FluxRTC server URL');
8
+ }
9
+ this.signalingUrl = signalingUrl;
10
+ this.socket = null;
11
+ this.device = null;
12
+ this.sendTransport = null;
13
+ this.recvTransport = null;
14
+ this.producers = new Map();
15
+ this.consumers = new Map();
16
+ this.localStream = null;
17
+ this.iceServers = [];
18
+ this.roomId = null;
19
+ this.token = null;
20
+ this._listeners = {};
21
+ }
22
+
23
+ on(event, fn) {
24
+ if (!this._listeners[event]) this._listeners[event] = [];
25
+ this._listeners[event].push(fn);
26
+ }
27
+
28
+ off(event, fn) {
29
+ if (!this._listeners[event]) return;
30
+ if (fn) {
31
+ this._listeners[event] = this._listeners[event].filter(f => f !== fn);
32
+ } else {
33
+ delete this._listeners[event];
34
+ }
35
+ }
36
+
37
+ _emit(event, ...args) {
38
+ (this._listeners[event] || []).forEach(fn => fn(...args));
39
+ }
40
+
41
+ connect(token) {
42
+ this.token = token;
43
+ return new Promise((resolve, reject) => {
44
+ this.socket = io(this.signalingUrl, {
45
+ transports: ['websocket'],
46
+ auth: { token },
47
+ });
48
+
49
+ this.socket.on('connect', () => {
50
+ this._emit('connected', { socketId: this.socket.id });
51
+ resolve();
52
+ });
53
+
54
+ this.socket.on('connect_error', (err) => {
55
+ this._emit('error', { type: 'connect', message: err.message });
56
+ reject(err);
57
+ });
58
+
59
+ this.socket.on('new-peer', (data) => {
60
+ this._emit('peerJoined', data);
61
+ });
62
+
63
+ this.socket.on('peer-left', (data) => {
64
+ this._emit('peerLeft', data);
65
+ });
66
+
67
+ this.socket.on('new-producer', (data) => {
68
+ this._emit('newProducer', data);
69
+ });
70
+
71
+ this.socket.on('disconnect', (reason) => {
72
+ this._emit('disconnected', { reason });
73
+ });
74
+ });
75
+ }
76
+
77
+ async fetchIceServers() {
78
+ return new Promise((resolve) => {
79
+ this.socket.emit('get-ice-servers', {}, (servers) => {
80
+ if (servers && servers.length) {
81
+ this.iceServers = servers;
82
+ } else {
83
+ this.iceServers = [{ urls: 'stun:stun.l.google.com:19302' }];
84
+ }
85
+ resolve(this.iceServers);
86
+ });
87
+ });
88
+ }
89
+
90
+ async joinRoom(roomId, token) {
91
+ this.roomId = roomId;
92
+ if (token) this.token = token;
93
+
94
+ return new Promise(async (resolve, reject) => {
95
+ try {
96
+ await this.fetchIceServers();
97
+
98
+ this.socket.emit('join-room', { roomId, token: this.token }, async (response) => {
99
+ try {
100
+ if (response.error) throw new Error(response.error);
101
+
102
+ this.device = new Device();
103
+ await this.device.load({ routerRtpCapabilities: response.rtpCapabilities });
104
+
105
+ await this._createSendTransport();
106
+ await this._createRecvTransport();
107
+
108
+ resolve({
109
+ peers: response.peers || [],
110
+ rtpCapabilities: response.rtpCapabilities,
111
+ });
112
+ } catch (err) {
113
+ reject(err);
114
+ }
115
+ });
116
+ } catch (err) {
117
+ reject(err);
118
+ }
119
+ });
120
+ }
121
+
122
+ async _createSendTransport() {
123
+ return new Promise((resolve, reject) => {
124
+ this.socket.emit('create-transport', { direction: 'send' }, (data) => {
125
+ if (data.error) return reject(new Error(data.error));
126
+
127
+ this.sendTransport = this.device.createSendTransport({
128
+ ...data,
129
+ iceServers: this.iceServers,
130
+ });
131
+
132
+ this.sendTransport.on('connect', ({ dtlsParameters }, callback, errback) => {
133
+ this.socket.emit('connect-transport', {
134
+ transportId: this.sendTransport.id,
135
+ dtlsParameters,
136
+ }, (res) => {
137
+ if (res.error) errback(new Error(res.error));
138
+ else callback();
139
+ });
140
+ });
141
+
142
+ this.sendTransport.on('produce', ({ kind, rtpParameters }, callback, errback) => {
143
+ this.socket.emit('produce', {
144
+ transportId: this.sendTransport.id,
145
+ kind,
146
+ rtpParameters,
147
+ }, (res) => {
148
+ if (res.error) errback(new Error(res.error));
149
+ else callback({ id: res.producerId });
150
+ });
151
+ });
152
+
153
+ resolve();
154
+ });
155
+ });
156
+ }
157
+
158
+ async _createRecvTransport() {
159
+ return new Promise((resolve, reject) => {
160
+ this.socket.emit('create-transport', { direction: 'recv' }, (data) => {
161
+ if (data.error) return reject(new Error(data.error));
162
+
163
+ this.recvTransport = this.device.createRecvTransport({
164
+ ...data,
165
+ iceServers: this.iceServers,
166
+ });
167
+
168
+ this.recvTransport.on('connect', ({ dtlsParameters }, callback, errback) => {
169
+ this.socket.emit('connect-transport', {
170
+ transportId: this.recvTransport.id,
171
+ dtlsParameters,
172
+ }, (res) => {
173
+ if (res.error) errback(new Error(res.error));
174
+ else callback();
175
+ });
176
+ });
177
+
178
+ resolve();
179
+ });
180
+ });
181
+ }
182
+
183
+ async enableMedia(constraints = { video: true, audio: true }) {
184
+ if (this.localStream) return this.localStream;
185
+
186
+ try {
187
+ this.localStream = await navigator.mediaDevices.getUserMedia(constraints);
188
+ return this.localStream;
189
+ } catch (err) {
190
+ if (constraints.video && (err.name === 'NotReadableError' || err.name === 'NotFoundError')) {
191
+ this.localStream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
192
+ return this.localStream;
193
+ }
194
+ throw err;
195
+ }
196
+ }
197
+
198
+ async produce() {
199
+ if (!this.localStream || !this.sendTransport) {
200
+ throw new Error('Call enableMedia() and joinRoom() before produce()');
201
+ }
202
+
203
+ const producers = [];
204
+ for (const track of this.localStream.getTracks()) {
205
+ const producer = await this.sendTransport.produce({ track });
206
+ this.producers.set(producer.id, producer);
207
+ producers.push({ id: producer.id, kind: track.kind });
208
+ }
209
+ return producers;
210
+ }
211
+
212
+ async consume(producerId) {
213
+ return new Promise((resolve, reject) => {
214
+ this.socket.emit('consume', {
215
+ producerId,
216
+ rtpCapabilities: this.device.rtpCapabilities,
217
+ }, async (data) => {
218
+ try {
219
+ if (data.error) throw new Error(data.error);
220
+
221
+ const consumer = await this.recvTransport.consume({
222
+ id: data.consumerId,
223
+ producerId: data.producerId,
224
+ kind: data.kind,
225
+ rtpParameters: data.rtpParameters,
226
+ });
227
+
228
+ this.consumers.set(consumer.id, consumer);
229
+ resolve({ consumer, track: consumer.track, kind: data.kind });
230
+ } catch (err) {
231
+ reject(err);
232
+ }
233
+ });
234
+ });
235
+ }
236
+
237
+ async consumeAll() {
238
+ return new Promise((resolve) => {
239
+ this.socket.emit('get-producers', {}, async (producers) => {
240
+ const results = [];
241
+ for (const p of producers) {
242
+ try {
243
+ const result = await this.consume(p.producerId);
244
+ results.push({ ...result, peerId: p.peerId, displayName: p.displayName });
245
+ } catch (_) {
246
+ // skip failed consumers
247
+ }
248
+ }
249
+ resolve(results);
250
+ });
251
+ });
252
+ }
253
+
254
+ async toggleVideo() {
255
+ if (!this.localStream) return false;
256
+ const videoTrack = this.localStream.getVideoTracks()[0];
257
+ if (videoTrack) {
258
+ videoTrack.enabled = !videoTrack.enabled;
259
+ return videoTrack.enabled;
260
+ }
261
+ try {
262
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
263
+ const newTrack = stream.getVideoTracks()[0];
264
+ this.localStream.addTrack(newTrack);
265
+ if (this.sendTransport) {
266
+ await this.sendTransport.produce({ track: newTrack });
267
+ }
268
+ return true;
269
+ } catch (err) {
270
+ return false;
271
+ }
272
+ }
273
+
274
+ toggleAudio() {
275
+ if (!this.localStream) return false;
276
+ const audioTrack = this.localStream.getAudioTracks()[0];
277
+ if (audioTrack) {
278
+ audioTrack.enabled = !audioTrack.enabled;
279
+ return audioTrack.enabled;
280
+ }
281
+ return false;
282
+ }
283
+
284
+ disconnect() {
285
+ this.producers.forEach(p => p.close());
286
+ this.consumers.forEach(c => c.close());
287
+ if (this.sendTransport) this.sendTransport.close();
288
+ if (this.recvTransport) this.recvTransport.close();
289
+ if (this.localStream) {
290
+ this.localStream.getTracks().forEach(t => t.stop());
291
+ }
292
+ if (this.socket) this.socket.disconnect();
293
+ this.localStream = null;
294
+ this.sendTransport = null;
295
+ this.recvTransport = null;
296
+ this.producers.clear();
297
+ this.consumers.clear();
298
+ this.token = null;
299
+ this.roomId = null;
300
+ this._emit('disconnected', { reason: 'manual' });
301
+ }
302
+ }
303
+
304
+ module.exports = { VideoClient };
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ const { VideoClient } = require('./VideoClient');
2
+ const { generateMeetingId } = require('./utils');
3
+
4
+ module.exports = { VideoClient, generateMeetingId };
package/src/utils.js ADDED
@@ -0,0 +1,7 @@
1
+ function generateMeetingId() {
2
+ const chars = 'abcdefghijklmnopqrstuvwxyz';
3
+ const seg = (len) => Array.from({ length: len }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
4
+ return seg(3) + '-' + seg(4) + '-' + seg(3);
5
+ }
6
+
7
+ module.exports = { generateMeetingId };