@afosecure/meetingsdk 1.4.3 → 1.4.5

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
@@ -164,10 +164,10 @@ 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;
170
169
  this.iceServers = [];
170
+ this.lastPong = Date.now();
171
171
  this.intentionalDisconnect = false;
172
172
  this.room = {
173
173
  id: null,
@@ -175,14 +175,11 @@ var VideoSDKCore = class {
175
175
  };
176
176
  this.localStream = null;
177
177
  this.screenStream = null;
178
+ this.screenSender = null;
178
179
  this.isScreenSharing = false;
179
- this.screenSenders = {};
180
180
  this.pingInterval = null;
181
- this.pendingIceCandidates = {};
182
- this.pendingOffers = {};
183
181
  this.reconnectAttempts = 0;
184
182
  this.participantName = "";
185
- // Track if we're in the waiting room (pending approval)
186
183
  this.isWaitingForApproval = false;
187
184
  this.pendingRequestId = null;
188
185
  this.iceTransportPolicy = "all";
@@ -192,29 +189,12 @@ var VideoSDKCore = class {
192
189
  this.myId = localStorage.getItem("vsdk_id") || crypto.randomUUID();
193
190
  localStorage.setItem("vsdk_id", this.myId);
194
191
  }
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 ----------------
192
+ // ---------------- MEDIA SETUP ----------------
210
193
  async initLocal(video, name) {
211
194
  this.participantName = name;
212
195
  try {
213
196
  this.localStream = await navigator.mediaDevices.getUserMedia({
214
- video: {
215
- width: { ideal: 1280 },
216
- height: { ideal: 720 }
217
- },
197
+ video: { width: { ideal: 1280 }, height: { ideal: 720 } },
218
198
  audio: {
219
199
  echoCancellation: true,
220
200
  noiseSuppression: true,
@@ -243,7 +223,115 @@ var VideoSDKCore = class {
243
223
  throw err;
244
224
  }
245
225
  }
246
- // ---------------- CONNECT ----------------
226
+ async joinMeeting(config) {
227
+ const { roomId, name, audioMuted = false, videoMuted = false } = config;
228
+ if (!roomId || !name) {
229
+ throw new Error("roomId and name are required to join meeting");
230
+ }
231
+ this.participantName = name;
232
+ if (!this.localStream) {
233
+ this.localStream = await navigator.mediaDevices.getUserMedia({
234
+ video: true,
235
+ audio: true
236
+ });
237
+ }
238
+ this.localStream.getAudioTracks().forEach((t) => t.enabled = !audioMuted);
239
+ this.localStream.getVideoTracks().forEach((t) => t.enabled = !videoMuted);
240
+ this.state.updateLocalParticipant({
241
+ id: this.myId,
242
+ name: this.participantName,
243
+ media: {
244
+ stream: this.localStream,
245
+ micEnabled: !audioMuted,
246
+ camEnabled: !videoMuted,
247
+ isScreenSharing: false
248
+ }
249
+ });
250
+ this.state.localStream = this.localStream;
251
+ await this.connect(roomId, name);
252
+ }
253
+ // ---------------- SFU PEER CONNECTION CREATION ----------------
254
+ setupPublisherPC() {
255
+ if (!this.localStream) return;
256
+ this.pubPC = new RTCPeerConnection({
257
+ iceServers: this.iceServers,
258
+ iceTransportPolicy: this.iceTransportPolicy
259
+ });
260
+ this.localStream.getTracks().forEach((track) => {
261
+ this.pubPC?.addTrack(track, this.localStream);
262
+ });
263
+ this.pubPC.onicecandidate = (e) => {
264
+ if (e.candidate) {
265
+ this.send({
266
+ type: "PUB_ICE",
267
+ payload: JSON.stringify(e.candidate),
268
+ user_id: this.myId
269
+ });
270
+ }
271
+ };
272
+ this.pubPC.onconnectionstatechange = () => {
273
+ console.log(`[SFU Publisher PC State]`, this.pubPC?.connectionState);
274
+ if (this.pubPC?.connectionState === "failed") {
275
+ this.restartPublisherIce();
276
+ }
277
+ };
278
+ }
279
+ setupSubscriberPC() {
280
+ this.subPC = new RTCPeerConnection({
281
+ iceServers: this.iceServers,
282
+ iceTransportPolicy: this.iceTransportPolicy
283
+ });
284
+ this.subPC.onicecandidate = (e) => {
285
+ if (e.candidate) {
286
+ this.send({
287
+ type: "SUB_ICE",
288
+ payload: JSON.stringify(e.candidate),
289
+ user_id: this.myId
290
+ });
291
+ }
292
+ };
293
+ this.subPC.ontrack = (event) => {
294
+ const incomingStream = event.streams[0] || new MediaStream([event.track]);
295
+ const streamId = incomingStream.id.replace(/[{}]/g, "");
296
+ let matchedParticipant;
297
+ for (const p of this.state.participants.values()) {
298
+ if (p.media?.cameraStreamId === streamId || p.media?.remoteScreenStreamId === streamId) {
299
+ matchedParticipant = p;
300
+ break;
301
+ }
302
+ }
303
+ if (!matchedParticipant) {
304
+ console.warn(
305
+ `[SFU ontrack] Dynamic track received for stream ${streamId}`
306
+ );
307
+ return;
308
+ }
309
+ const pId = matchedParticipant.id;
310
+ const isScreen = streamId === matchedParticipant.media?.remoteScreenStreamId;
311
+ if (isScreen) {
312
+ this.state.updateParticipantMedia(pId, {
313
+ screenStream: incomingStream,
314
+ screenTrack: event.track,
315
+ isScreenSharing: true
316
+ });
317
+ if (!this.state.presenterId) {
318
+ this.state.setPresenterId(pId);
319
+ }
320
+ this.events.onScreenShareStarted?.(pId, incomingStream);
321
+ } else {
322
+ this.state.updateParticipantMedia(pId, {
323
+ stream: incomingStream,
324
+ cameraTrack: incomingStream.getVideoTracks()[0],
325
+ audioTrack: incomingStream.getAudioTracks()[0]
326
+ });
327
+ this.events.onTrack?.(incomingStream, pId);
328
+ }
329
+ };
330
+ this.subPC.onconnectionstatechange = () => {
331
+ console.log(`[SFU Subscriber PC State]`, this.subPC?.connectionState);
332
+ };
333
+ }
334
+ // ---------------- WEBSOCKET CONNECTION & SIGNALING ----------------
247
335
  async connect(roomId, name) {
248
336
  this.room.id = roomId;
249
337
  this.reset();
@@ -252,15 +340,17 @@ var VideoSDKCore = class {
252
340
  this.joinRejecter = reject;
253
341
  this.ws = new WebSocket(this.url);
254
342
  this.ws.onopen = () => {
255
- console.log("WebSocket connected, sending JOIN...");
343
+ console.log("WebSocket connected to SFU, sending JOIN...");
344
+ const micEnabled = !!this.state.localParticipant?.media?.micEnabled;
345
+ const camEnabled = !!this.state.localParticipant?.media?.camEnabled;
256
346
  this.send({
257
347
  type: "JOIN",
258
348
  room_id: roomId,
259
349
  user_id: this.myId,
260
350
  sender_name: name,
261
351
  camera_stream_id: this.localStream?.id.replace(/[{}]/g, ""),
262
- audio_muted: this.state.localParticipant?.media?.micEnabled,
263
- video_muted: this.state.localParticipant?.media?.camEnabled
352
+ audio_muted: !micEnabled,
353
+ video_muted: !camEnabled
264
354
  });
265
355
  };
266
356
  this.ws.onerror = (err) => {
@@ -273,13 +363,7 @@ var VideoSDKCore = class {
273
363
  raw: e
274
364
  });
275
365
  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) {
366
+ if (this.intentionalDisconnect || e.code === 1e3 || e.code === 1001 || this.isWaitingForApproval) {
283
367
  return;
284
368
  }
285
369
  this.scheduleReconnect();
@@ -289,203 +373,70 @@ var VideoSDKCore = class {
289
373
  };
290
374
  });
291
375
  }
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
376
  async handle(msg) {
421
- var _a, _b, _c, _d;
422
377
  if (msg.sender === this.myId) return;
423
378
  switch (msg.type) {
424
379
  case "PONG":
425
380
  this.lastPong = Date.now();
426
381
  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;
382
+ case "JOINED": {
383
+ if (msg.iceServers) {
384
+ this.iceServers = msg.iceServers;
446
385
  }
447
- try {
448
- await pc.setRemoteDescription({
386
+ this.room.name = msg.room_name;
387
+ this.isWaitingForApproval = false;
388
+ this.pendingRequestId = null;
389
+ this.intentionalDisconnect = false;
390
+ this.reconnectAttempts = 0;
391
+ this.setupPublisherPC();
392
+ this.setupSubscriberPC();
393
+ await this.createPublisherOffer();
394
+ this.startHeartbeat();
395
+ this.joinResolver?.();
396
+ this.joinResolver = void 0;
397
+ this.joinRejecter = void 0;
398
+ break;
399
+ }
400
+ case "SFU_PUB_ANSWER": {
401
+ if (this.pubPC) {
402
+ await this.pubPC.setRemoteDescription({
449
403
  type: "answer",
450
404
  sdp: msg.payload
451
405
  });
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
406
  }
462
407
  break;
463
408
  }
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;
409
+ case "SFU_SUB_OFFER": {
410
+ if (this.subPC) {
411
+ await this.subPC.setRemoteDescription({
412
+ type: "offer",
413
+ sdp: msg.payload
414
+ });
415
+ const answer = await this.subPC.createAnswer();
416
+ await this.subPC.setLocalDescription(answer);
417
+ this.send({
418
+ type: "SUB_ANSWER",
419
+ payload: answer.sdp,
420
+ user_id: this.myId
421
+ });
471
422
  }
472
- if (!pc.remoteDescription) {
473
- (_c = this.pendingIceCandidates)[_d = msg.sender] ?? (_c[_d] = []);
474
- this.pendingIceCandidates[msg.sender].push(candidate);
475
- break;
423
+ break;
424
+ }
425
+ case "PUB_ICE": {
426
+ if (this.pubPC && msg.payload) {
427
+ await this.pubPC.addIceCandidate(JSON.parse(msg.payload)).catch(console.warn);
476
428
  }
477
- try {
478
- await pc.addIceCandidate(candidate);
479
- } catch (err) {
480
- console.warn("ICE error:", err);
429
+ break;
430
+ }
431
+ case "SUB_ICE": {
432
+ if (this.subPC && msg.payload) {
433
+ await this.subPC.addIceCandidate(JSON.parse(msg.payload)).catch(console.warn);
481
434
  }
482
435
  break;
483
436
  }
484
- case "EXISTING_USERS":
437
+ case "EXISTING_USERS": {
485
438
  if (msg.presenterId) {
486
439
  this.state.setPresenterId(msg.presenterId);
487
- this.events.onScreenShareStarted?.(msg.presenterId, null);
488
- this.state.setPresenterId(msg.presenterId);
489
440
  }
490
441
  for (const p of msg.participants || []) {
491
442
  if (!p?.id || p.id === this.myId) continue;
@@ -497,9 +448,6 @@ var VideoSDKCore = class {
497
448
  media: {
498
449
  stream: null,
499
450
  screenStream: void 0,
500
- cameraTrack: void 0,
501
- screenTrack: void 0,
502
- audioTrack: void 0,
503
451
  micEnabled: p.micEnabled ?? true,
504
452
  camEnabled: p.camEnabled ?? true,
505
453
  isScreenSharing: p.isScreenSharing ?? false,
@@ -509,118 +457,53 @@ var VideoSDKCore = class {
509
457
  };
510
458
  this.state.addParticipant(structuredParticipant);
511
459
  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
460
  }
524
461
  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
462
  }
564
463
  case "USER_JOINED": {
565
464
  const p = msg.participant;
566
465
  if (!p?.id || p.id === this.myId) return;
567
466
  this.state.addParticipant(p);
568
467
  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
468
  break;
607
469
  }
608
- case "USER_LEFT":
470
+ case "USER_LEFT": {
609
471
  const peerId = msg.participant.id;
610
- this.closePeer(peerId);
611
472
  this.state.removeParticipant(peerId);
612
473
  this.events.onUserLeft?.(peerId);
613
474
  break;
475
+ }
614
476
  case "MEDIA_STATE_CHANGE": {
615
- const peerId2 = msg.peerId;
477
+ const peerId = msg.peerId;
616
478
  const { kind, enabled } = msg;
617
479
  if (kind === "audio") {
618
- this.state.updateParticipantMedia(peerId2, { micEnabled: enabled });
619
- this.events.onMicToggled?.(peerId2, enabled);
480
+ this.state.updateParticipantMedia(peerId, { micEnabled: enabled });
481
+ this.events.onMicToggled?.(peerId, enabled);
620
482
  } else if (kind === "video") {
621
- this.state.updateParticipantMedia(peerId2, { camEnabled: enabled });
622
- this.events.onCamToggled?.(peerId2, enabled);
483
+ this.state.updateParticipantMedia(peerId, { camEnabled: enabled });
484
+ this.events.onCamToggled?.(peerId, enabled);
485
+ }
486
+ break;
487
+ }
488
+ case "SCREEN_SHARE_START": {
489
+ const peerId = msg.peerId;
490
+ this.state.updateParticipantMedia(peerId, {
491
+ isScreenSharing: true,
492
+ remoteScreenStreamId: msg.stream_id,
493
+ cameraStreamId: msg?.camera_stream_id
494
+ });
495
+ if (!this.state.presenterId) {
496
+ this.state.setPresenterId(peerId);
497
+ }
498
+ break;
499
+ }
500
+ case "SCREEN_SHARE_STOP": {
501
+ const peerId = msg.peerId;
502
+ this.state.updateParticipantMedia(peerId, { isScreenSharing: false });
503
+ if (this.state.presenterId === peerId) {
504
+ this.state.setPresenterId(null);
623
505
  }
506
+ this.events.onScreenShareStopped?.(peerId);
624
507
  break;
625
508
  }
626
509
  case "CHAT_MESSAGE": {
@@ -637,27 +520,37 @@ var VideoSDKCore = class {
637
520
  this.events.onChatMessage?.(msg);
638
521
  break;
639
522
  }
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
523
+ case "JOIN_PENDING": {
524
+ const req = msg.request;
525
+ this.isWaitingForApproval = true;
526
+ this.pendingRequestId = req.request_id;
527
+ this.events.onEntryRequested?.({
528
+ requestId: req.request_id,
529
+ userId: req.user_id,
530
+ name: req.name
646
531
  });
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
532
  break;
653
533
  }
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);
534
+ case "JOIN_APPROVED": {
535
+ this.isWaitingForApproval = false;
536
+ this.pendingRequestId = null;
537
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
538
+ this.send({
539
+ type: "JOIN",
540
+ room_id: this.room.id,
541
+ user_id: this.myId,
542
+ sender_name: this.participantName
543
+ });
659
544
  }
660
- this.events.onScreenShareStopped?.(peerId2);
545
+ break;
546
+ }
547
+ case "JOIN_REJECTED": {
548
+ this.isWaitingForApproval = false;
549
+ this.pendingRequestId = null;
550
+ this.events.onEntryResponded?.({
551
+ participantId: msg.user_id,
552
+ decision: "rejected"
553
+ });
661
554
  break;
662
555
  }
663
556
  case "ERROR": {
@@ -668,259 +561,78 @@ var VideoSDKCore = class {
668
561
  msg,
669
562
  !fatal
670
563
  );
671
- if (fatal) {
672
- this.disconnect();
673
- }
564
+ if (fatal) this.disconnect();
674
565
  return;
675
566
  }
676
567
  }
677
568
  }
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];
569
+ // ---------------- PUBLISHER RENEGOTIATION ----------------
570
+ async createPublisherOffer() {
571
+ if (!this.pubPC) return;
796
572
  try {
797
- const offer = await pc.createOffer();
798
- await pc.setLocalDescription(offer);
573
+ const offer = await this.pubPC.createOffer();
574
+ await this.pubPC.setLocalDescription(offer);
799
575
  this.send({
800
- type: "OFFER",
576
+ type: "PUB_OFFER",
801
577
  payload: offer.sdp,
802
- sender: this.myId,
803
- target: id
578
+ user_id: this.myId,
579
+ room_id: this.room.id
804
580
  });
805
- console.debug(`[Offer] Sent to ${id}`);
806
581
  } catch (err) {
807
- console.error(`[Offer] Failed for ${id}:`, err);
582
+ console.error("[SFU Publisher Offer Error]", err);
808
583
  }
809
584
  }
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
- }
585
+ // ---------------- MEDIA TOGGLES ----------------
586
+ toggleMic() {
587
+ const mediaState = this.state.localParticipant?.media;
588
+ if (!mediaState) return;
589
+ const nextEnabled = !mediaState.micEnabled;
590
+ this.localStream?.getAudioTracks().forEach((t) => t.enabled = nextEnabled);
591
+ this.state.updateLocalParticipant({
592
+ id: this.myId,
593
+ name: this.participantName,
594
+ media: { ...mediaState, micEnabled: nextEnabled }
595
+ });
596
+ this.send({ type: "MEDIA_STATE", kind: "audio", enabled: nextEnabled });
879
597
  }
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);
598
+ toggleCam() {
599
+ const mediaState = this.state.localParticipant?.media;
600
+ if (!mediaState) return;
601
+ const nextEnabled = !mediaState.camEnabled;
602
+ this.localStream?.getVideoTracks().forEach((t) => t.enabled = nextEnabled);
603
+ this.state.updateLocalParticipant({
604
+ id: this.myId,
605
+ name: this.participantName,
606
+ media: { ...mediaState, camEnabled: nextEnabled }
607
+ });
608
+ this.send({ type: "MEDIA_STATE", kind: "video", enabled: nextEnabled });
891
609
  }
610
+ // ---------------- SCREEN SHARING (SFU) ----------------
892
611
  async startScreenShare() {
893
612
  try {
894
613
  if (this.state.presenterId && this.state.presenterId !== this.myId) {
895
614
  throw new Error("Another user is already sharing their screen.");
896
615
  }
897
- if (!navigator.mediaDevices?.getDisplayMedia) {
898
- throw new Error("Screen sharing not supported on this device");
899
- }
900
616
  this.screenStream = await navigator.mediaDevices.getDisplayMedia({
901
617
  video: true
902
- // audio: true,
903
618
  });
904
619
  this.isScreenSharing = true;
620
+ const screenTrack = this.screenStream.getVideoTracks()[0];
621
+ if (this.pubPC) {
622
+ this.screenSender = this.pubPC.addTrack(screenTrack, this.screenStream);
623
+ await this.createPublisherOffer();
624
+ }
905
625
  this.state.updateLocalParticipant({
906
626
  media: {
907
627
  isScreenSharing: true,
908
628
  screenStream: this.screenStream,
909
- screenTrack: this.screenStream.getVideoTracks()[0]
629
+ screenTrack
910
630
  }
911
631
  });
912
632
  this.state.setPresenterId(this.myId);
913
- this.screenStream.getVideoTracks()[0].onended = () => {
633
+ screenTrack.onended = () => {
914
634
  this.stopScreenShare();
915
635
  };
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
636
  this.send({
925
637
  type: "SCREEN_SHARE_START",
926
638
  sender: this.myId,
@@ -932,7 +644,7 @@ var VideoSDKCore = class {
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
  );
@@ -944,18 +656,15 @@ var VideoSDKCore = class {
944
656
  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
+ try {
661
+ this.pubPC.removeTrack(this.screenSender);
662
+ this.createPublisherOffer();
663
+ } catch (e) {
664
+ console.warn("Failed removing screen sender", e);
665
+ }
666
+ this.screenSender = null;
667
+ }
959
668
  this.screenStream = null;
960
669
  this.isScreenSharing = false;
961
670
  this.state.updateLocalParticipant({
@@ -974,15 +683,10 @@ var VideoSDKCore = class {
974
683
  room_id: this.room.id
975
684
  });
976
685
  }
686
+ // ---------------- CHAT & RECONNECT ----------------
977
687
  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");
688
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.room.id)
984
689
  return;
985
- }
986
690
  const isPrivate = !!payload?.target;
987
691
  const senderName = this.state.localParticipant?.name || "Anonymous";
988
692
  const msg = {
@@ -1006,12 +710,48 @@ var VideoSDKCore = class {
1006
710
  client_ts: Date.now()
1007
711
  });
1008
712
  }
713
+ scheduleReconnect() {
714
+ if (!this.room.id) return;
715
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
716
+ window.clearTimeout(this.reconnectTimer);
717
+ this.reconnectTimer = window.setTimeout(async () => {
718
+ try {
719
+ await this.connect(this.room.id, this.participantName);
720
+ this.reconnectAttempts = 0;
721
+ } catch {
722
+ this.reconnectAttempts++;
723
+ this.scheduleReconnect();
724
+ }
725
+ }, delay);
726
+ }
727
+ startHeartbeat() {
728
+ this.stopHeartbeat();
729
+ this.pingInterval = setInterval(() => {
730
+ if (this.ws?.readyState === WebSocket.OPEN) {
731
+ this.send({ type: "PING", client_ts: Date.now() });
732
+ }
733
+ }, 2e4);
734
+ }
735
+ stopHeartbeat() {
736
+ if (this.pingInterval) {
737
+ clearInterval(this.pingInterval);
738
+ this.pingInterval = null;
739
+ }
740
+ }
741
+ reset() {
742
+ this.pubPC?.close();
743
+ this.subPC?.close();
744
+ this.pubPC = null;
745
+ this.subPC = null;
746
+ this.state.resetRemoteState();
747
+ }
1009
748
  disconnect() {
1010
749
  this.intentionalDisconnect = true;
1011
750
  this.stopScreenShare();
1012
- Object.values(this.peers).forEach((pc) => pc.close());
1013
- this.peers = {};
1014
- this.initiators.clear();
751
+ this.pubPC?.close();
752
+ this.subPC?.close();
753
+ this.pubPC = null;
754
+ this.subPC = null;
1015
755
  this.stopHeartbeat();
1016
756
  if (this.ws?.readyState === WebSocket.OPEN) {
1017
757
  this.send({
@@ -1038,32 +778,40 @@ var VideoSDKCore = class {
1038
778
  this.state.clearChat();
1039
779
  this.state.setPresenterId(null);
1040
780
  }
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
- }
781
+ async restartPublisherIce() {
782
+ if (!this.pubPC) return;
783
+ try {
784
+ this.pubPC.restartIce();
785
+ await this.createPublisherOffer();
786
+ } catch (err) {
787
+ console.error("[Publisher ICE Restart Failed]", err);
1050
788
  }
1051
- delete this.pendingIceCandidates[id];
789
+ }
790
+ emitError(code, message, raw, recoverable = true) {
791
+ const err = {
792
+ code,
793
+ message,
794
+ raw,
795
+ roomId: this.room.id,
796
+ userId: this.myId,
797
+ recoverable
798
+ };
799
+ this.events.onError?.(err);
800
+ this.joinRejecter?.(err);
801
+ this.joinRejecter = void 0;
802
+ console.error("[MeetingSDK Error]", err);
1052
803
  }
1053
804
  send(msg) {
1054
805
  this.ws?.send(JSON.stringify(msg));
1055
806
  }
1056
807
  approveJoinRequest(requestId) {
1057
- this.send({
1058
- type: "JOIN_APPROVE",
1059
- request_id: requestId
1060
- });
808
+ this.send({ type: "JOIN_APPROVE", request_id: requestId });
1061
809
  }
1062
810
  rejectJoinRequest(requestId) {
1063
- this.send({
1064
- type: "JOIN_REJECT",
1065
- request_id: requestId
1066
- });
811
+ this.send({ type: "JOIN_REJECT", request_id: requestId });
812
+ }
813
+ getMeeting() {
814
+ return this.room;
1067
815
  }
1068
816
  };
1069
817
 
@@ -1320,40 +1068,74 @@ var useRemoteMedia = (participantId) => {
1320
1068
  };
1321
1069
 
1322
1070
  // src/react/useMeetingPreview.ts
1323
- import { useCallback as useCallback2, useEffect as useEffect6, useState as useState5 } from "react";
1071
+ import { useEffect as useEffect6, useState as useState5 } from "react";
1324
1072
  function useMeetingPreview(roomId, userId) {
1325
1073
  const [room, setRoom] = useState5(null);
1326
- const [isLoading, setIsLoading] = useState5(true);
1327
1074
  const [error, setError] = useState5(null);
1328
- const load = useCallback2(async () => {
1329
- if (!roomId || !userId) return;
1330
- try {
1331
- setIsLoading(true);
1075
+ const [isConnected, setIsConnected] = useState5(false);
1076
+ const [isLoading, setIsLoading] = useState5(true);
1077
+ useEffect6(() => {
1078
+ if (!roomId || !userId) {
1079
+ setIsLoading(false);
1080
+ return;
1081
+ }
1082
+ let ws = null;
1083
+ const heartbeat = setInterval(() => {
1084
+ if (ws && ws.readyState === WebSocket.OPEN) {
1085
+ ws.send(
1086
+ JSON.stringify({
1087
+ type: "PING"
1088
+ })
1089
+ );
1090
+ }
1091
+ }, 2e4);
1092
+ ws = new WebSocket(`${SDK_CONFIG.wsUrl}/watch/${roomId}?user_id=${userId}`);
1093
+ ws.onopen = () => {
1094
+ setIsConnected(true);
1332
1095
  setError(null);
1333
- const res = await fetch(
1334
- `${SDK_CONFIG.baseUrl}/api/rooms/${roomId}/live?user_id=${userId}`
1335
- );
1336
- if (!res.ok) {
1337
- throw new Error("Failed to fetch meeting preview");
1096
+ console.log("[Preview] watcher connected");
1097
+ };
1098
+ ws.onmessage = (event) => {
1099
+ try {
1100
+ const msg = JSON.parse(event.data);
1101
+ if (msg.type !== "ROOM_PRESENCE_UPDATE") {
1102
+ return;
1103
+ }
1104
+ setRoom({
1105
+ active: msg.active ?? false,
1106
+ count: msg.count ?? 0,
1107
+ canJoin: msg.canJoin ?? false,
1108
+ approved: msg.approved ?? false,
1109
+ isHost: msg.isHost ?? false,
1110
+ hasMoreParticipants: msg.hasMoreParticipants ?? false,
1111
+ participants: msg.participants ?? []
1112
+ });
1113
+ setIsLoading(false);
1114
+ } catch (err) {
1115
+ console.error("Invalid room presence payload", err);
1116
+ setIsLoading(false);
1338
1117
  }
1339
- const data = await res.json();
1340
- setRoom(data);
1341
- } catch (err) {
1342
- setError(err instanceof Error ? err : new Error("Unknown error"));
1343
- } finally {
1118
+ };
1119
+ ws.onerror = () => {
1120
+ setError("Failed to connect to room monitor");
1344
1121
  setIsLoading(false);
1345
- }
1122
+ };
1123
+ ws.onclose = () => {
1124
+ setIsConnected(false);
1125
+ console.log("[Preview] disconnected");
1126
+ };
1127
+ return () => {
1128
+ clearInterval(heartbeat);
1129
+ if (ws) {
1130
+ ws.close(1e3, "Leaving preview");
1131
+ }
1132
+ };
1346
1133
  }, [roomId, userId]);
1347
- useEffect6(() => {
1348
- load();
1349
- const interval = setInterval(load, 5e3);
1350
- return () => clearInterval(interval);
1351
- }, [load]);
1352
1134
  return {
1353
1135
  room,
1136
+ isConnected,
1354
1137
  isLoading,
1355
- error,
1356
- refetch: load
1138
+ error
1357
1139
  };
1358
1140
  }
1359
1141
  export {