@afosecure/meetingsdk 1.4.4 → 1.4.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.mjs CHANGED
@@ -6,8 +6,8 @@ import { createContext, useContext, useMemo, useRef } from "react";
6
6
 
7
7
  // src/config/ws.ts
8
8
  var SDK_CONFIG = {
9
- wsUrl: "wss://rust-video-server-sfyf.onrender.com/ws",
10
- baseUrl: "https://rust-video-server-sfyf.onrender.com"
9
+ wsUrl: "wss://localhost:8080/ws",
10
+ baseUrl: "https://localhost:8080"
11
11
  };
12
12
 
13
13
  // src/core/MeetingState.ts
@@ -164,10 +164,11 @@ var VideoSDKCore = class {
164
164
  this.events = events;
165
165
  this.url = url;
166
166
  this.ws = null;
167
- this.peers = {};
168
- this.initiators = /* @__PURE__ */ new Set();
169
- this.lastPong = Date.now();
167
+ this.pubPC = null;
168
+ this.subPC = null;
169
+ this.pendingTracks = [];
170
170
  this.iceServers = [];
171
+ this.lastPong = Date.now();
171
172
  this.intentionalDisconnect = false;
172
173
  this.room = {
173
174
  id: null,
@@ -175,14 +176,11 @@ var VideoSDKCore = class {
175
176
  };
176
177
  this.localStream = null;
177
178
  this.screenStream = null;
179
+ this.screenSender = null;
178
180
  this.isScreenSharing = false;
179
- this.screenSenders = {};
180
181
  this.pingInterval = null;
181
- this.pendingIceCandidates = {};
182
- this.pendingOffers = {};
183
182
  this.reconnectAttempts = 0;
184
183
  this.participantName = "";
185
- // Track if we're in the waiting room (pending approval)
186
184
  this.isWaitingForApproval = false;
187
185
  this.pendingRequestId = null;
188
186
  this.iceTransportPolicy = "all";
@@ -192,29 +190,12 @@ var VideoSDKCore = class {
192
190
  this.myId = localStorage.getItem("vsdk_id") || crypto.randomUUID();
193
191
  localStorage.setItem("vsdk_id", this.myId);
194
192
  }
195
- emitError(code, message, raw, recoverable = true) {
196
- const err = {
197
- code,
198
- message,
199
- raw,
200
- roomId: this.room.id,
201
- userId: this.myId,
202
- recoverable
203
- };
204
- this.events.onError?.(err);
205
- this.joinRejecter?.(err);
206
- this.joinRejecter = void 0;
207
- console.error("[MeetingSDK Error]", err);
208
- }
209
- // ---------------- STREAM ----------------
193
+ // ---------------- MEDIA SETUP ----------------
210
194
  async initLocal(video, name) {
211
195
  this.participantName = name;
212
196
  try {
213
197
  this.localStream = await navigator.mediaDevices.getUserMedia({
214
- video: {
215
- width: { ideal: 1280 },
216
- height: { ideal: 720 }
217
- },
198
+ video: { width: { ideal: 1280 }, height: { ideal: 720 } },
218
199
  audio: {
219
200
  echoCancellation: true,
220
201
  noiseSuppression: true,
@@ -232,6 +213,8 @@ var VideoSDKCore = class {
232
213
  name: this.participantName,
233
214
  media: {
234
215
  stream: this.localStream,
216
+ cameraTrack: this.localStream.getVideoTracks()[0],
217
+ audioTrack: this.localStream.getAudioTracks()[0],
235
218
  micEnabled: true,
236
219
  camEnabled: true,
237
220
  isScreenSharing: false
@@ -243,7 +226,109 @@ var VideoSDKCore = class {
243
226
  throw err;
244
227
  }
245
228
  }
246
- // ---------------- CONNECT ----------------
229
+ async joinMeeting(config) {
230
+ const { roomId, name, audioMuted = false, videoMuted = false } = config;
231
+ if (!roomId || !name) {
232
+ throw new Error("roomId and name are required to join meeting");
233
+ }
234
+ this.participantName = name;
235
+ if (!this.localStream) {
236
+ this.localStream = await navigator.mediaDevices.getUserMedia({
237
+ video: true,
238
+ audio: true
239
+ });
240
+ }
241
+ this.localStream.getAudioTracks().forEach((t) => t.enabled = !audioMuted);
242
+ this.localStream.getVideoTracks().forEach((t) => t.enabled = !videoMuted);
243
+ this.state.updateLocalParticipant({
244
+ id: this.myId,
245
+ name: this.participantName,
246
+ media: {
247
+ stream: this.localStream,
248
+ cameraTrack: this.localStream.getVideoTracks()[0],
249
+ audioTrack: this.localStream.getAudioTracks()[0],
250
+ micEnabled: !audioMuted,
251
+ camEnabled: !videoMuted,
252
+ isScreenSharing: false
253
+ }
254
+ });
255
+ this.state.localStream = this.localStream;
256
+ await this.connect(roomId, name);
257
+ }
258
+ // ---------------- SFU PEER CONNECTION CREATION ----------------
259
+ setupPublisherPC() {
260
+ if (!this.localStream) return;
261
+ this.pubPC = new RTCPeerConnection({
262
+ iceServers: this.iceServers,
263
+ iceTransportPolicy: this.iceTransportPolicy
264
+ });
265
+ this.localStream.getTracks().forEach((track) => {
266
+ this.pubPC?.addTrack(track, this.localStream);
267
+ });
268
+ this.pubPC.onicecandidate = (e) => {
269
+ if (e.candidate) {
270
+ this.send({
271
+ type: "PUB_ICE",
272
+ payload: JSON.stringify(e.candidate),
273
+ user_id: this.myId
274
+ });
275
+ }
276
+ };
277
+ this.pubPC.onconnectionstatechange = () => {
278
+ console.log(`[SFU Publisher PC State]`, this.pubPC?.connectionState);
279
+ if (this.pubPC?.connectionState === "failed") {
280
+ this.restartPublisherIce();
281
+ }
282
+ };
283
+ }
284
+ setupSubscriberPC() {
285
+ this.subPC = new RTCPeerConnection({
286
+ iceServers: this.iceServers,
287
+ iceTransportPolicy: this.iceTransportPolicy
288
+ });
289
+ this.subPC.onicecandidate = (e) => {
290
+ if (e.candidate) {
291
+ this.send({
292
+ type: "SUB_ICE",
293
+ payload: JSON.stringify(e.candidate),
294
+ user_id: this.myId
295
+ });
296
+ }
297
+ };
298
+ this.subPC.ontrack = (event) => {
299
+ const descriptor = this.pendingTracks.shift();
300
+ if (!descriptor) {
301
+ console.warn("Unknown incoming track");
302
+ return;
303
+ }
304
+ const stream = event.streams[0] || new MediaStream([event.track]);
305
+ switch (descriptor.source) {
306
+ case "camera":
307
+ this.state.updateParticipantMedia(descriptor.publisher_id, {
308
+ stream,
309
+ cameraTrack: event.track
310
+ });
311
+ break;
312
+ case "screen":
313
+ this.state.updateParticipantMedia(descriptor.publisher_id, {
314
+ screenStream: stream,
315
+ screenTrack: event.track,
316
+ isScreenSharing: true
317
+ });
318
+ break;
319
+ case "audio":
320
+ this.state.updateParticipantMedia(descriptor.publisher_id, {
321
+ stream,
322
+ audioTrack: event.track
323
+ });
324
+ break;
325
+ }
326
+ };
327
+ this.subPC.onconnectionstatechange = () => {
328
+ console.log(`[SFU Subscriber PC State]`, this.subPC?.connectionState);
329
+ };
330
+ }
331
+ // ---------------- WEBSOCKET CONNECTION & SIGNALING ----------------
247
332
  async connect(roomId, name) {
248
333
  this.room.id = roomId;
249
334
  this.reset();
@@ -252,15 +337,17 @@ var VideoSDKCore = class {
252
337
  this.joinRejecter = reject;
253
338
  this.ws = new WebSocket(this.url);
254
339
  this.ws.onopen = () => {
255
- console.log("WebSocket connected, sending JOIN...");
340
+ console.log("WebSocket connected to SFU, sending JOIN...");
341
+ const micEnabled = !!this.state.localParticipant?.media?.micEnabled;
342
+ const camEnabled = !!this.state.localParticipant?.media?.camEnabled;
256
343
  this.send({
257
344
  type: "JOIN",
258
345
  room_id: roomId,
259
346
  user_id: this.myId,
260
347
  sender_name: name,
261
348
  camera_stream_id: this.localStream?.id.replace(/[{}]/g, ""),
262
- audio_muted: this.state.localParticipant?.media?.micEnabled,
263
- video_muted: this.state.localParticipant?.media?.camEnabled
349
+ audio_muted: !micEnabled,
350
+ video_muted: !camEnabled
264
351
  });
265
352
  };
266
353
  this.ws.onerror = (err) => {
@@ -273,13 +360,7 @@ var VideoSDKCore = class {
273
360
  raw: e
274
361
  });
275
362
  this.joinRejecter = void 0;
276
- if (this.intentionalDisconnect) {
277
- return;
278
- }
279
- if (e.code === 1e3 || e.code === 1001) {
280
- return;
281
- }
282
- if (this.isWaitingForApproval) {
363
+ if (this.intentionalDisconnect || e.code === 1e3 || e.code === 1001 || this.isWaitingForApproval) {
283
364
  return;
284
365
  }
285
366
  this.scheduleReconnect();
@@ -289,203 +370,71 @@ var VideoSDKCore = class {
289
370
  };
290
371
  });
291
372
  }
292
- async joinMeeting(config) {
293
- const { roomId, name, audioMuted = false, videoMuted = false } = config;
294
- if (!roomId || !name) {
295
- throw new Error("roomId and name are required to join meeting");
296
- }
297
- this.participantName = name;
298
- if (!this.localStream) {
299
- this.localStream = await navigator.mediaDevices.getUserMedia({
300
- video: true,
301
- audio: true
302
- });
303
- }
304
- this.localStream.getAudioTracks().forEach((t) => {
305
- t.enabled = !audioMuted;
306
- });
307
- this.localStream.getVideoTracks().forEach((t) => {
308
- t.enabled = !videoMuted;
309
- });
310
- this.state.updateLocalParticipant({
311
- id: this.myId,
312
- name: this.participantName,
313
- media: {
314
- stream: this.localStream,
315
- micEnabled: !audioMuted,
316
- camEnabled: !videoMuted,
317
- isScreenSharing: false
318
- }
319
- });
320
- this.state.localStream = this.localStream;
321
- await this.connect(roomId, name);
322
- }
323
- getMeeting() {
324
- return this.room;
325
- }
326
- toggleMic() {
327
- const mediaState = this.state.localParticipant?.media;
328
- if (!mediaState) return;
329
- const nextEnabled = !mediaState.micEnabled;
330
- this.localStream?.getAudioTracks().forEach((t) => t.enabled = nextEnabled);
331
- this.state.updateLocalParticipant({
332
- id: this.myId,
333
- name: this.participantName,
334
- media: {
335
- ...mediaState,
336
- micEnabled: nextEnabled
337
- }
338
- });
339
- this.send({
340
- type: "MEDIA_STATE",
341
- kind: "audio",
342
- enabled: nextEnabled
343
- });
344
- }
345
- toggleCam() {
346
- const mediaState = this.state.localParticipant?.media;
347
- if (!mediaState) return;
348
- const nextEnabled = !mediaState.camEnabled;
349
- this.localStream?.getVideoTracks().forEach((t) => t.enabled = nextEnabled);
350
- this.state.updateLocalParticipant({
351
- id: this.myId,
352
- name: this.participantName,
353
- media: {
354
- ...mediaState,
355
- camEnabled: nextEnabled
356
- }
357
- });
358
- this.send({
359
- type: "MEDIA_STATE",
360
- kind: "video",
361
- enabled: nextEnabled
362
- });
363
- }
364
- scheduleReconnect() {
365
- if (!this.room.id) return;
366
- const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
367
- clearTimeout(this.reconnectTimer);
368
- this.reconnectTimer = window.setTimeout(async () => {
369
- try {
370
- await this.connect(this.room.id, this.participantName);
371
- this.reconnectAttempts = 0;
372
- } catch {
373
- this.reconnectAttempts++;
374
- this.scheduleReconnect();
375
- }
376
- }, delay);
377
- }
378
- startHeartbeat() {
379
- this.stopHeartbeat();
380
- this.pingInterval = setInterval(() => {
381
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
382
- this.send({
383
- type: "PING",
384
- client_ts: Date.now()
385
- });
386
- }, 2e4);
387
- }
388
- stopHeartbeat() {
389
- if (this.pingInterval) {
390
- clearInterval(this.pingInterval);
391
- this.pingInterval = null;
392
- }
393
- }
394
- // ---------------- RESET ----------------
395
- reset() {
396
- Object.values(this.peers).forEach((pc) => pc.close());
397
- this.peers = {};
398
- this.initiators.clear();
399
- this.pendingIceCandidates = {};
400
- this.state.resetRemoteState();
401
- }
402
- async handleJoinApproved(msg) {
403
- console.log("JOIN_APPROVED received, sending new JOIN...");
404
- this.events.onEntryResponded?.({
405
- participantId: msg.user_id,
406
- decision: "approved"
407
- });
408
- this.isWaitingForApproval = false;
409
- this.pendingRequestId = null;
410
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
411
- this.send({
412
- type: "JOIN",
413
- room_id: this.room.id,
414
- user_id: this.myId,
415
- sender_name: this.participantName
416
- });
417
- console.log("Sent new JOIN after approval");
418
- }
419
- }
420
373
  async handle(msg) {
421
- var _a, _b, _c, _d;
422
374
  if (msg.sender === this.myId) return;
423
375
  switch (msg.type) {
424
376
  case "PONG":
425
377
  this.lastPong = Date.now();
426
378
  break;
427
- case "OFFER":
428
- console.log("[Offer] Received from", msg.sender, {
429
- sdp: msg.payload.substring(0, 200)
430
- });
431
- await this.handleOffer(msg.payload, msg.sender);
432
- break;
433
- case "ANSWER": {
434
- const pc = this.peers[msg.sender];
435
- console.log("[Answer] Received from", msg.sender, {
436
- signalingState: pc?.signalingState,
437
- iceConnectionState: pc?.iceConnectionState,
438
- connectionState: pc?.connectionState
439
- });
440
- if (!pc) return;
441
- if (pc.signalingState !== "have-local-offer") {
442
- console.warn(
443
- `[Signaling] Unexpected ANSWER in state "${pc.signalingState}", ignoring`
444
- );
445
- return;
379
+ case "JOINED": {
380
+ if (msg.iceServers) {
381
+ this.iceServers = msg.iceServers;
446
382
  }
447
- try {
448
- await pc.setRemoteDescription({
383
+ this.room.name = msg.room_name;
384
+ this.isWaitingForApproval = false;
385
+ this.pendingRequestId = null;
386
+ this.intentionalDisconnect = false;
387
+ this.reconnectAttempts = 0;
388
+ this.setupPublisherPC();
389
+ this.setupSubscriberPC();
390
+ await this.createPublisherOffer();
391
+ this.startHeartbeat();
392
+ this.joinResolver?.();
393
+ this.joinResolver = void 0;
394
+ this.joinRejecter = void 0;
395
+ break;
396
+ }
397
+ case "PUB_ANSWER": {
398
+ if (this.pubPC) {
399
+ await this.pubPC.setRemoteDescription({
449
400
  type: "answer",
450
401
  sdp: msg.payload
451
402
  });
452
- await this.flushIce(msg.sender, pc);
453
- } catch (err) {
454
- console.error("[Signaling] Failed to apply answer:", err);
455
- this.emitError(
456
- "ANSWER_FAILED",
457
- `Failed to apply answer from ${msg.sender}`,
458
- err,
459
- true
460
- );
461
403
  }
462
404
  break;
463
405
  }
464
- case "ICE": {
465
- const candidate = JSON.parse(msg.payload);
466
- let pc = this.peers[msg.sender];
467
- if (!pc) {
468
- (_a = this.pendingIceCandidates)[_b = msg.sender] ?? (_a[_b] = []);
469
- this.pendingIceCandidates[msg.sender].push(candidate);
470
- break;
406
+ case "SUB_OFFER": {
407
+ this.pendingTracks.push(msg.track);
408
+ if (this.subPC) {
409
+ await this.subPC.setRemoteDescription({
410
+ type: "offer",
411
+ sdp: msg.payload
412
+ });
413
+ const answer = await this.subPC.createAnswer();
414
+ await this.subPC.setLocalDescription(answer);
415
+ this.send({
416
+ type: "SUB_ANSWER",
417
+ payload: answer.sdp,
418
+ user_id: this.myId
419
+ });
471
420
  }
472
- if (!pc.remoteDescription) {
473
- (_c = this.pendingIceCandidates)[_d = msg.sender] ?? (_c[_d] = []);
474
- this.pendingIceCandidates[msg.sender].push(candidate);
475
- break;
421
+ break;
422
+ }
423
+ case "PUB_ICE": {
424
+ if (this.pubPC && msg.payload) {
425
+ await this.pubPC.addIceCandidate(JSON.parse(msg.payload)).catch(console.warn);
476
426
  }
477
- try {
478
- await pc.addIceCandidate(candidate);
479
- } catch (err) {
480
- console.warn("ICE error:", err);
427
+ break;
428
+ }
429
+ case "SUB_ICE": {
430
+ if (this.subPC && msg.payload) {
431
+ await this.subPC.addIceCandidate(JSON.parse(msg.payload)).catch(console.warn);
481
432
  }
482
433
  break;
483
434
  }
484
- case "EXISTING_USERS":
435
+ case "EXISTING_USERS": {
485
436
  if (msg.presenterId) {
486
437
  this.state.setPresenterId(msg.presenterId);
487
- this.events.onScreenShareStarted?.(msg.presenterId, null);
488
- this.state.setPresenterId(msg.presenterId);
489
438
  }
490
439
  for (const p of msg.participants || []) {
491
440
  if (!p?.id || p.id === this.myId) continue;
@@ -497,9 +446,6 @@ var VideoSDKCore = class {
497
446
  media: {
498
447
  stream: null,
499
448
  screenStream: void 0,
500
- cameraTrack: void 0,
501
- screenTrack: void 0,
502
- audioTrack: void 0,
503
449
  micEnabled: p.micEnabled ?? true,
504
450
  camEnabled: p.camEnabled ?? true,
505
451
  isScreenSharing: p.isScreenSharing ?? false,
@@ -509,118 +455,53 @@ var VideoSDKCore = class {
509
455
  };
510
456
  this.state.addParticipant(structuredParticipant);
511
457
  this.events.onUserJoined?.(structuredParticipant);
512
- if (p.isScreenSharing && p.remoteScreenStreamId) {
513
- this.state.setPresenterId(p.id);
514
- this.state.updateParticipantMedia(p.id, {
515
- isScreenSharing: true,
516
- remoteScreenStreamId: p.remoteScreenStreamId,
517
- cameraStreamId: p.cameraId || null
518
- });
519
- }
520
- if (this.shouldInitiate(p.id)) {
521
- await this.createOffer(p.id);
522
- }
523
458
  }
524
459
  break;
525
- case "JOINED": {
526
- if (msg.iceServers) {
527
- this.iceServers = msg.iceServers;
528
- }
529
- this.room.name = msg.room_name;
530
- this.isWaitingForApproval = false;
531
- this.pendingRequestId = null;
532
- this.intentionalDisconnect = false;
533
- this.reconnectAttempts = 0;
534
- if (Object.keys(this.pendingOffers).length > 0) {
535
- console.log(
536
- "Processing",
537
- Object.keys(this.pendingOffers).length,
538
- "pending offers"
539
- );
540
- for (const [peerId2, sdp] of Object.entries(this.pendingOffers)) {
541
- await this.handleOffer(sdp, peerId2);
542
- }
543
- this.pendingOffers = {};
544
- }
545
- const media = this.state.localParticipant?.media;
546
- if (media) {
547
- this.send({
548
- type: "MEDIA_STATE",
549
- kind: "audio",
550
- enabled: !!media.micEnabled
551
- });
552
- this.send({
553
- type: "MEDIA_STATE",
554
- kind: "video",
555
- enabled: !!media.camEnabled
556
- });
557
- }
558
- this.startHeartbeat();
559
- this.joinResolver?.();
560
- this.joinResolver = void 0;
561
- this.joinRejecter = void 0;
562
- break;
563
460
  }
564
461
  case "USER_JOINED": {
565
462
  const p = msg.participant;
566
463
  if (!p?.id || p.id === this.myId) return;
567
464
  this.state.addParticipant(p);
568
465
  this.events.onUserJoined?.(p);
569
- if (this.shouldInitiate(p.id)) {
570
- await this.createOffer(p.id);
571
- }
572
- break;
573
- }
574
- case "JOIN_PENDING": {
575
- const req = msg.request;
576
- this.isWaitingForApproval = true;
577
- this.pendingRequestId = req.request_id;
578
- this.events.onEntryRequested?.({
579
- requestId: req.request_id,
580
- userId: req.user_id,
581
- name: req.name
582
- });
583
- break;
584
- }
585
- case "JOIN_REQUEST": {
586
- const req = msg.request;
587
- this.events.onEntryRequested?.({
588
- requestId: req.id,
589
- userId: req.user_id,
590
- name: req.name
591
- });
592
- break;
593
- }
594
- case "JOIN_APPROVED": {
595
- await this.handleJoinApproved(msg);
596
- break;
597
- }
598
- case "JOIN_REJECTED": {
599
- const decision = "rejected";
600
- this.isWaitingForApproval = false;
601
- this.pendingRequestId = null;
602
- this.events.onEntryResponded?.({
603
- participantId: msg.user_id,
604
- decision
605
- });
606
466
  break;
607
467
  }
608
- case "USER_LEFT":
468
+ case "USER_LEFT": {
609
469
  const peerId = msg.participant.id;
610
- this.closePeer(peerId);
611
470
  this.state.removeParticipant(peerId);
612
471
  this.events.onUserLeft?.(peerId);
613
472
  break;
473
+ }
614
474
  case "MEDIA_STATE_CHANGE": {
615
- const peerId2 = msg.peerId;
475
+ const peerId = msg.peerId;
616
476
  const { kind, enabled } = msg;
617
477
  if (kind === "audio") {
618
- this.state.updateParticipantMedia(peerId2, { micEnabled: enabled });
619
- this.events.onMicToggled?.(peerId2, enabled);
478
+ this.state.updateParticipantMedia(peerId, { micEnabled: enabled });
479
+ this.events.onMicToggled?.(peerId, enabled);
620
480
  } else if (kind === "video") {
621
- this.state.updateParticipantMedia(peerId2, { camEnabled: enabled });
622
- this.events.onCamToggled?.(peerId2, enabled);
481
+ this.state.updateParticipantMedia(peerId, { camEnabled: enabled });
482
+ this.events.onCamToggled?.(peerId, enabled);
483
+ }
484
+ break;
485
+ }
486
+ case "SCREEN_SHARE_START": {
487
+ const peerId = msg.peerId;
488
+ this.state.updateParticipantMedia(peerId, {
489
+ isScreenSharing: true,
490
+ remoteScreenStreamId: msg.stream_id,
491
+ cameraStreamId: msg?.camera_stream_id
492
+ });
493
+ if (!this.state.presenterId) {
494
+ this.state.setPresenterId(peerId);
495
+ }
496
+ break;
497
+ }
498
+ case "SCREEN_SHARE_STOP": {
499
+ const peerId = msg.peerId;
500
+ this.state.updateParticipantMedia(peerId, { isScreenSharing: false });
501
+ if (this.state.presenterId === peerId) {
502
+ this.state.setPresenterId(null);
623
503
  }
504
+ this.events.onScreenShareStopped?.(peerId);
624
505
  break;
625
506
  }
626
507
  case "CHAT_MESSAGE": {
@@ -637,27 +518,37 @@ var VideoSDKCore = class {
637
518
  this.events.onChatMessage?.(msg);
638
519
  break;
639
520
  }
640
- case "SCREEN_SHARE_START": {
641
- const peerId2 = msg.peerId;
642
- this.state.updateParticipantMedia(peerId2, {
643
- isScreenSharing: true,
644
- remoteScreenStreamId: msg.stream_id,
645
- cameraStreamId: msg?.camera_stream_id
521
+ case "JOIN_PENDING": {
522
+ const req = msg.request;
523
+ this.isWaitingForApproval = true;
524
+ this.pendingRequestId = req.request_id;
525
+ this.events.onEntryRequested?.({
526
+ requestId: req.request_id,
527
+ userId: req.user_id,
528
+ name: req.name
646
529
  });
647
- if (!this.state.presenterId) {
648
- this.state.setPresenterId(peerId2);
649
- }
650
- const screenStream = this.state.getParticipant(peerId2)?.media?.screenStream;
651
- this.events.onScreenShareStarted?.(peerId2, screenStream || null);
652
530
  break;
653
531
  }
654
- case "SCREEN_SHARE_STOP": {
655
- const peerId2 = msg.peerId;
656
- this.state.updateParticipantMedia(peerId2, { isScreenSharing: false });
657
- if (this.state.presenterId === peerId2) {
658
- this.state.setPresenterId(null);
532
+ case "JOIN_APPROVED": {
533
+ this.isWaitingForApproval = false;
534
+ this.pendingRequestId = null;
535
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
536
+ this.send({
537
+ type: "JOIN",
538
+ room_id: this.room.id,
539
+ user_id: this.myId,
540
+ sender_name: this.participantName
541
+ });
659
542
  }
660
- this.events.onScreenShareStopped?.(peerId2);
543
+ break;
544
+ }
545
+ case "JOIN_REJECTED": {
546
+ this.isWaitingForApproval = false;
547
+ this.pendingRequestId = null;
548
+ this.events.onEntryResponded?.({
549
+ participantId: msg.user_id,
550
+ decision: "rejected"
551
+ });
661
552
  break;
662
553
  }
663
554
  case "ERROR": {
@@ -668,271 +559,92 @@ var VideoSDKCore = class {
668
559
  msg,
669
560
  !fatal
670
561
  );
671
- if (fatal) {
672
- this.disconnect();
673
- }
562
+ if (fatal) this.disconnect();
674
563
  return;
675
564
  }
676
565
  }
677
566
  }
678
- // ---------------- PEER ----------------
679
- createPeer(id) {
680
- if (!this.localStream) throw new Error("No local stream");
681
- if (!this.iceServers || this.iceServers.length === 0) {
682
- throw new Error(
683
- "ICE Servers not configured. Backend must provide iceServers on JOIN."
684
- );
685
- }
686
- const pc = new RTCPeerConnection({
687
- iceServers: this.iceServers,
688
- iceTransportPolicy: this.iceTransportPolicy
689
- });
690
- pc.ontrack = (event) => {
691
- const incomingStream = event.streams?.[0] || new MediaStream([event.track]);
692
- const streamId = incomingStream?.id?.replace(/[{}]/g, "");
693
- const participant = this.state.getParticipant(id);
694
- const isCameraStream = streamId && participant?.media?.cameraStreamId && streamId === participant?.media?.cameraStreamId;
695
- const isScreenStream = streamId && participant?.media?.remoteScreenStreamId && streamId === participant?.media?.remoteScreenStreamId;
696
- if (event.track.muted) {
697
- event.track.onunmute = () => {
698
- console.log(`${event.track.kind} track unmuted for ${id}`);
699
- };
700
- }
701
- if (isScreenStream) {
702
- const videoTrack = event.track.kind === "video" ? event.track : incomingStream.getVideoTracks()[0] || participant?.media?.screenTrack;
703
- this.state.updateParticipantMedia(id, {
704
- screenStream: incomingStream,
705
- screenTrack: videoTrack,
706
- remoteScreenStreamId: incomingStream.id,
707
- isScreenSharing: true
708
- });
709
- if (!this.state.presenterId) {
710
- this.state.setPresenterId(id);
711
- }
712
- this.events.onScreenShareStarted?.(id, incomingStream);
713
- console.log(`Screen share stream detected for ${id}`);
714
- } else if (isCameraStream) {
715
- this.state.updateParticipantMedia(id, {
716
- stream: incomingStream,
717
- cameraTrack: incomingStream.getVideoTracks()[0],
718
- audioTrack: incomingStream.getAudioTracks()[0]
719
- });
720
- this.events.onTrack?.(incomingStream, id);
721
- } else {
722
- this.state.updateParticipantMedia(id, {
723
- stream: incomingStream,
724
- cameraTrack: incomingStream.getVideoTracks()[0],
725
- audioTrack: incomingStream.getAudioTracks()[0]
726
- });
727
- this.events.onTrack?.(incomingStream, id);
728
- }
729
- };
730
- pc.onicecandidate = (e) => {
731
- if (!e.candidate) return;
732
- console.log("LOCAL ICE:", e.candidate.candidate);
733
- this.send({
734
- type: "ICE",
735
- payload: JSON.stringify(e.candidate),
736
- sender: this.myId,
737
- target: id
738
- });
739
- };
740
- pc.oniceconnectionstatechange = async () => {
741
- console.log("================================");
742
- console.log(`Peer: ${id}`);
743
- console.log(`ICE: ${pc.iceConnectionState}`);
744
- console.log(`Connection: ${pc.connectionState}`);
745
- console.log(`Signaling: ${pc.signalingState}`);
746
- if (pc.iceConnectionState === "disconnected") {
747
- const stats = await pc.getStats();
748
- stats.forEach((report) => {
749
- if (report.type === "transport") {
750
- console.log(JSON.stringify(report, null, 2));
751
- }
752
- });
753
- }
754
- };
755
- pc.onconnectionstatechange = () => {
756
- if (pc.connectionState === "failed") {
757
- try {
758
- pc.restartIce();
759
- } catch {
760
- }
761
- }
762
- };
763
- this.localStream.getTracks().forEach((track) => {
764
- pc.addTrack(track, this.localStream);
765
- });
766
- if (this.isScreenSharing && this.screenStream) {
767
- this.screenSenders[id] = [];
768
- this.screenStream.getTracks().forEach((track) => {
769
- const sender = pc.addTrack(track, this.screenStream);
770
- this.screenSenders[id].push(sender);
771
- });
772
- }
773
- return pc;
774
- }
775
- // ---------------- OFFER ----------------
776
- async createOffer(id, isRenegotiation = false) {
777
- if (!isRenegotiation && !this.shouldInitiate(id)) {
778
- console.debug(
779
- `[Offer] ${id} should initiate (${id} > ${this.myId}), skipping`
780
- );
781
- return;
782
- }
783
- if (!isRenegotiation && this.initiators.has(id)) {
784
- console.debug(
785
- `[Offer] Already initiating with ${id}, skipping duplicate`
786
- );
787
- return;
788
- }
789
- if (!isRenegotiation) {
790
- this.initiators.add(id);
791
- }
792
- if (!this.peers[id]) {
793
- this.peers[id] = this.createPeer(id);
794
- }
795
- const pc = this.peers[id];
567
+ // ---------------- PUBLISHER RENEGOTIATION ----------------
568
+ async createPublisherOffer() {
569
+ if (!this.pubPC) return;
796
570
  try {
797
- const offer = await pc.createOffer();
798
- await pc.setLocalDescription(offer);
571
+ const offer = await this.pubPC.createOffer();
572
+ await this.pubPC.setLocalDescription(offer);
799
573
  this.send({
800
- type: "OFFER",
574
+ type: "PUB_OFFER",
801
575
  payload: offer.sdp,
802
- sender: this.myId,
803
- target: id
576
+ user_id: this.myId,
577
+ room_id: this.room.id
804
578
  });
805
- console.debug(`[Offer] Sent to ${id}`);
806
579
  } catch (err) {
807
- console.error(`[Offer] Failed for ${id}:`, err);
580
+ console.error("[SFU Publisher Offer Error]", err);
808
581
  }
809
582
  }
810
- shouldInitiate(peerId) {
811
- return this.myId < peerId;
812
- }
813
- // ---------------- ANSWER ----------------
814
- async handleOffer(sdp, id) {
815
- if (!this.iceServers || this.iceServers.length === 0) {
816
- console.warn("[Offer] Waiting for iceServers, queuing offer from", id);
817
- this.pendingOffers[id] = sdp;
818
- return;
819
- }
820
- if (!this.peers[id]) {
821
- this.peers[id] = this.createPeer(id);
822
- }
823
- const pc = this.peers[id];
824
- try {
825
- if (pc.signalingState === "have-local-offer") {
826
- if (this.shouldInitiate(id)) {
827
- console.warn(
828
- `[Glare] Both sent OFFERs, we win (${this.myId} < ${id}), keeping our OFFER`
829
- );
830
- return;
831
- } else {
832
- console.warn(
833
- `[Glare] Both sent OFFERs, they win (${id} < ${this.myId}), rolling back`
834
- );
835
- pc.close();
836
- delete this.peers[id];
837
- this.initiators.delete(id);
838
- this.peers[id] = this.createPeer(id);
839
- }
840
- }
841
- if (this.peers[id].signalingState !== "stable" && this.peers[id].signalingState !== "have-local-offer") {
842
- console.warn(
843
- `[Signaling] Cannot accept OFFER in state "${this.peers[id].signalingState}"`
844
- );
845
- return;
846
- }
847
- await this.peers[id].setRemoteDescription({
848
- type: "offer",
849
- sdp
850
- });
851
- const pending = this.pendingIceCandidates[id] || [];
852
- for (const candidate of pending) {
853
- try {
854
- await this.peers[id].addIceCandidate(candidate);
855
- } catch (err) {
856
- console.warn("[ICE] Failed to add candidate:", err);
857
- }
858
- }
859
- delete this.pendingIceCandidates[id];
860
- const answer = await this.peers[id].createAnswer();
861
- await this.peers[id].setLocalDescription(answer);
862
- await this.flushIce(id, this.peers[id]);
863
- this.send({
864
- type: "ANSWER",
865
- payload: answer.sdp,
866
- sender: this.myId,
867
- target: id
868
- });
869
- console.debug(`[Answer] Sent to ${id}`);
870
- } catch (err) {
871
- console.error(`[Signaling] Failed to handle OFFER from ${id}:`, err);
872
- this.emitError(
873
- "OFFER_HANDLING_FAILED",
874
- `Failed to handle offer from ${id}`,
875
- err,
876
- true
877
- );
878
- }
583
+ // ---------------- MEDIA TOGGLES ----------------
584
+ toggleMic() {
585
+ const mediaState = this.state.localParticipant?.media;
586
+ if (!mediaState) return;
587
+ const nextEnabled = !mediaState.micEnabled;
588
+ this.localStream?.getAudioTracks().forEach((t) => t.enabled = nextEnabled);
589
+ this.state.updateLocalParticipant({
590
+ id: this.myId,
591
+ name: this.participantName,
592
+ media: { ...mediaState, micEnabled: nextEnabled }
593
+ });
594
+ this.send({ type: "MEDIA_STATE", kind: "audio", enabled: nextEnabled });
879
595
  }
880
- // ---------------- CLEANUP ----------------
881
- closePeer(id) {
882
- const pc = this.peers[id];
883
- if (!pc) return;
884
- pc.ontrack = null;
885
- pc.onicecandidate = null;
886
- pc.onconnectionstatechange = null;
887
- pc.close();
888
- delete this.peers[id];
889
- this.initiators.delete(id);
890
- this.state.removeParticipant(id);
596
+ toggleCam() {
597
+ const mediaState = this.state.localParticipant?.media;
598
+ if (!mediaState) return;
599
+ const nextEnabled = !mediaState.camEnabled;
600
+ this.localStream?.getVideoTracks().forEach((t) => t.enabled = nextEnabled);
601
+ this.state.updateLocalParticipant({
602
+ id: this.myId,
603
+ name: this.participantName,
604
+ media: { ...mediaState, camEnabled: nextEnabled }
605
+ });
606
+ this.send({ type: "MEDIA_STATE", kind: "video", enabled: nextEnabled });
891
607
  }
608
+ // ---------------- SCREEN SHARING (SFU) ----------------
892
609
  async startScreenShare() {
893
610
  try {
894
611
  if (this.state.presenterId && this.state.presenterId !== this.myId) {
895
612
  throw new Error("Another user is already sharing their screen.");
896
613
  }
897
- if (!navigator.mediaDevices?.getDisplayMedia) {
898
- throw new Error("Screen sharing not supported on this device");
899
- }
900
614
  this.screenStream = await navigator.mediaDevices.getDisplayMedia({
901
615
  video: true
902
- // audio: true,
903
616
  });
904
617
  this.isScreenSharing = true;
618
+ const screenTrack = this.screenStream.getVideoTracks()[0];
619
+ Object.defineProperty(screenTrack, "contentHint", {
620
+ value: "detail"
621
+ });
622
+ if (this.pubPC) {
623
+ this.screenSender = this.pubPC.addTrack(screenTrack, this.screenStream);
624
+ await this.createPublisherOffer();
625
+ }
905
626
  this.state.updateLocalParticipant({
906
627
  media: {
907
628
  isScreenSharing: true,
908
629
  screenStream: this.screenStream,
909
- screenTrack: this.screenStream.getVideoTracks()[0]
630
+ screenTrack
910
631
  }
911
632
  });
912
633
  this.state.setPresenterId(this.myId);
913
- this.screenStream.getVideoTracks()[0].onended = () => {
634
+ screenTrack.onended = () => {
914
635
  this.stopScreenShare();
915
636
  };
916
- Object.entries(this.peers).forEach(([peerId, pc]) => {
917
- this.screenSenders[peerId] = [];
918
- this.screenStream.getTracks().forEach((track) => {
919
- const sender = pc.addTrack(track, this.screenStream);
920
- this.screenSenders[peerId].push(sender);
921
- });
922
- this.createOffer(peerId, true);
923
- });
924
637
  this.send({
925
638
  type: "SCREEN_SHARE_START",
926
639
  sender: this.myId,
927
640
  room_id: this.room.id,
928
- camera_id: this.localStream?.id.replace(/[{}]/g, ""),
929
641
  stream_id: this.screenStream.id.replace(/[{}]/g, "")
930
642
  });
931
643
  return this.screenStream;
932
644
  } catch (err) {
933
645
  this.emitError(
934
646
  "SCREEN_SHARE_FAILED",
935
- err?.message || "Failed to start screen sharing",
647
+ err?.message || "Failed screen share",
936
648
  err,
937
649
  true
938
650
  );
@@ -941,21 +653,14 @@ var VideoSDKCore = class {
941
653
  throw err;
942
654
  }
943
655
  }
944
- stopScreenShare() {
656
+ async stopScreenShare() {
945
657
  if (!this.screenStream) return;
946
658
  this.screenStream.getTracks().forEach((t) => t.stop());
947
- Object.entries(this.peers).forEach(([peerId, pc]) => {
948
- const senders = this.screenSenders[peerId] || [];
949
- senders.forEach((sender) => {
950
- try {
951
- pc.removeTrack(sender);
952
- } catch (err) {
953
- console.warn(err);
954
- }
955
- });
956
- delete this.screenSenders[peerId];
957
- this.createOffer(peerId, true);
958
- });
659
+ if (this.pubPC && this.screenSender) {
660
+ this.pubPC.removeTrack(this.screenSender);
661
+ this.screenSender = null;
662
+ await this.createPublisherOffer();
663
+ }
959
664
  this.screenStream = null;
960
665
  this.isScreenSharing = false;
961
666
  this.state.updateLocalParticipant({
@@ -974,15 +679,10 @@ var VideoSDKCore = class {
974
679
  room_id: this.room.id
975
680
  });
976
681
  }
682
+ // ---------------- CHAT & RECONNECT ----------------
977
683
  sendChatMessage(payload) {
978
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
979
- console.warn("WS not connected");
980
- return;
981
- }
982
- if (!this.room.id) {
983
- console.warn("No roomId set");
684
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.room.id)
984
685
  return;
985
- }
986
686
  const isPrivate = !!payload?.target;
987
687
  const senderName = this.state.localParticipant?.name || "Anonymous";
988
688
  const msg = {
@@ -1006,12 +706,48 @@ var VideoSDKCore = class {
1006
706
  client_ts: Date.now()
1007
707
  });
1008
708
  }
709
+ scheduleReconnect() {
710
+ if (!this.room.id) return;
711
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
712
+ window.clearTimeout(this.reconnectTimer);
713
+ this.reconnectTimer = window.setTimeout(async () => {
714
+ try {
715
+ await this.connect(this.room.id, this.participantName);
716
+ this.reconnectAttempts = 0;
717
+ } catch {
718
+ this.reconnectAttempts++;
719
+ this.scheduleReconnect();
720
+ }
721
+ }, delay);
722
+ }
723
+ startHeartbeat() {
724
+ this.stopHeartbeat();
725
+ this.pingInterval = setInterval(() => {
726
+ if (this.ws?.readyState === WebSocket.OPEN) {
727
+ this.send({ type: "PING", client_ts: Date.now() });
728
+ }
729
+ }, 2e4);
730
+ }
731
+ stopHeartbeat() {
732
+ if (this.pingInterval) {
733
+ clearInterval(this.pingInterval);
734
+ this.pingInterval = null;
735
+ }
736
+ }
737
+ reset() {
738
+ this.pubPC?.close();
739
+ this.subPC?.close();
740
+ this.pubPC = null;
741
+ this.subPC = null;
742
+ this.state.resetRemoteState();
743
+ }
1009
744
  disconnect() {
1010
745
  this.intentionalDisconnect = true;
1011
746
  this.stopScreenShare();
1012
- Object.values(this.peers).forEach((pc) => pc.close());
1013
- this.peers = {};
1014
- this.initiators.clear();
747
+ this.pubPC?.close();
748
+ this.subPC?.close();
749
+ this.pubPC = null;
750
+ this.subPC = null;
1015
751
  this.stopHeartbeat();
1016
752
  if (this.ws?.readyState === WebSocket.OPEN) {
1017
753
  this.send({
@@ -1038,32 +774,40 @@ var VideoSDKCore = class {
1038
774
  this.state.clearChat();
1039
775
  this.state.setPresenterId(null);
1040
776
  }
1041
- async flushIce(id, pc) {
1042
- const pending = this.pendingIceCandidates[id];
1043
- if (!pending?.length) return;
1044
- for (const candidate of pending) {
1045
- try {
1046
- await pc.addIceCandidate(candidate);
1047
- } catch (e) {
1048
- console.warn("ICE flush error", e);
1049
- }
777
+ async restartPublisherIce() {
778
+ if (!this.pubPC) return;
779
+ try {
780
+ this.pubPC.restartIce();
781
+ await this.createPublisherOffer();
782
+ } catch (err) {
783
+ console.error("[Publisher ICE Restart Failed]", err);
1050
784
  }
1051
- delete this.pendingIceCandidates[id];
785
+ }
786
+ emitError(code, message, raw, recoverable = true) {
787
+ const err = {
788
+ code,
789
+ message,
790
+ raw,
791
+ roomId: this.room.id,
792
+ userId: this.myId,
793
+ recoverable
794
+ };
795
+ this.events.onError?.(err);
796
+ this.joinRejecter?.(err);
797
+ this.joinRejecter = void 0;
798
+ console.error("[MeetingSDK Error]", err);
1052
799
  }
1053
800
  send(msg) {
1054
801
  this.ws?.send(JSON.stringify(msg));
1055
802
  }
1056
803
  approveJoinRequest(requestId) {
1057
- this.send({
1058
- type: "JOIN_APPROVE",
1059
- request_id: requestId
1060
- });
804
+ this.send({ type: "JOIN_APPROVE", request_id: requestId });
1061
805
  }
1062
806
  rejectJoinRequest(requestId) {
1063
- this.send({
1064
- type: "JOIN_REJECT",
1065
- request_id: requestId
1066
- });
807
+ this.send({ type: "JOIN_REJECT", request_id: requestId });
808
+ }
809
+ getMeeting() {
810
+ return this.room;
1067
811
  }
1068
812
  };
1069
813