@afosecure/meetingsdk 1.4.5 → 1.4.7

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://sfuserver-production.up.railway.app/ws",
10
+ baseUrl: "https://sfuserver-production.up.railway.app"
11
11
  };
12
12
 
13
13
  // src/core/MeetingState.ts
@@ -166,6 +166,9 @@ var VideoSDKCore = class {
166
166
  this.ws = null;
167
167
  this.pubPC = null;
168
168
  this.subPC = null;
169
+ this.pendingTracks = /* @__PURE__ */ new Map();
170
+ this.subscriberNegotiating = false;
171
+ this.subscriberOfferQueue = [];
169
172
  this.iceServers = [];
170
173
  this.lastPong = Date.now();
171
174
  this.intentionalDisconnect = false;
@@ -189,31 +192,84 @@ var VideoSDKCore = class {
189
192
  this.myId = localStorage.getItem("vsdk_id") || crypto.randomUUID();
190
193
  localStorage.setItem("vsdk_id", this.myId);
191
194
  }
195
+ async acquireLocalMedia(options) {
196
+ const { videoConstraints = true, audioConstraints = true } = options;
197
+ try {
198
+ const stream = await navigator.mediaDevices.getUserMedia({
199
+ video: videoConstraints,
200
+ audio: audioConstraints
201
+ });
202
+ const hasVideo = stream.getVideoTracks().some((t) => t.readyState === "live");
203
+ const hasAudio = stream.getAudioTracks().some((t) => t.readyState === "live");
204
+ return { stream, camEnabled: hasVideo, micEnabled: hasAudio };
205
+ } catch (err) {
206
+ console.warn(
207
+ "[VideoSDKCore] Primary getUserMedia failed:",
208
+ err?.name || err?.message
209
+ );
210
+ const isDeviceLocked = err?.name === "NotReadableError" || err?.name === "TrackStartError" || err?.message?.toLowerCase().includes("allocate videosource") || err?.message?.toLowerCase().includes("could not start video source");
211
+ if (isDeviceLocked) {
212
+ console.warn(
213
+ "[VideoSDKCore] Camera is locked by another browser/app. Falling back to Audio-Only."
214
+ );
215
+ try {
216
+ const audioOnlyStream = await navigator.mediaDevices.getUserMedia({
217
+ video: false,
218
+ audio: audioConstraints
219
+ });
220
+ return {
221
+ stream: audioOnlyStream,
222
+ camEnabled: false,
223
+ micEnabled: audioOnlyStream.getAudioTracks().some((t) => t.readyState === "live")
224
+ };
225
+ } catch (audioErr) {
226
+ console.warn(
227
+ "[VideoSDKCore] Audio acquisition also failed. Falling back to View-Only stream."
228
+ );
229
+ }
230
+ }
231
+ return {
232
+ stream: new MediaStream(),
233
+ camEnabled: false,
234
+ micEnabled: false
235
+ };
236
+ }
237
+ }
192
238
  // ---------------- MEDIA SETUP ----------------
193
239
  async initLocal(video, name) {
194
240
  this.participantName = name;
195
241
  try {
196
- this.localStream = await navigator.mediaDevices.getUserMedia({
197
- video: { width: { ideal: 1280 }, height: { ideal: 720 } },
198
- audio: {
242
+ const { stream, camEnabled, micEnabled } = await this.acquireLocalMedia({
243
+ videoConstraints: { width: { ideal: 1280 }, height: { ideal: 720 } },
244
+ audioConstraints: {
199
245
  echoCancellation: true,
200
246
  noiseSuppression: true,
201
247
  autoGainControl: true
202
248
  }
203
249
  });
204
- const hasVideo = this.localStream.getVideoTracks().some((t) => t.readyState === "live");
205
- const hasAudio = this.localStream.getAudioTracks().some((t) => t.readyState === "live");
206
- if (!hasVideo || !hasAudio) {
207
- throw new Error(`Missing tracks: video=${hasVideo}, audio=${hasAudio}`);
250
+ this.localStream = stream;
251
+ console.log(
252
+ "[LOCAL MEDIA]",
253
+ this.localStream.getTracks().map((t) => ({
254
+ kind: t.kind,
255
+ enabled: t.enabled,
256
+ ready: t.readyState
257
+ }))
258
+ );
259
+ if (video && this.localStream.getVideoTracks().length > 0) {
260
+ video.srcObject = this.localStream;
208
261
  }
209
- video.srcObject = this.localStream;
262
+ const cameraTrack = this.localStream.getVideoTracks()[0] || void 0;
263
+ const audioTrack = this.localStream.getAudioTracks()[0] || void 0;
210
264
  this.state.updateLocalParticipant({
211
265
  id: this.myId,
212
266
  name: this.participantName,
213
267
  media: {
214
268
  stream: this.localStream,
215
- micEnabled: true,
216
- camEnabled: true,
269
+ cameraTrack,
270
+ audioTrack,
271
+ micEnabled,
272
+ camEnabled,
217
273
  isScreenSharing: false
218
274
  }
219
275
  });
@@ -229,21 +285,38 @@ var VideoSDKCore = class {
229
285
  throw new Error("roomId and name are required to join meeting");
230
286
  }
231
287
  this.participantName = name;
288
+ let camEnabled = !videoMuted;
289
+ let micEnabled = !audioMuted;
232
290
  if (!this.localStream) {
233
- this.localStream = await navigator.mediaDevices.getUserMedia({
234
- video: true,
235
- audio: true
291
+ const acquired = await this.acquireLocalMedia({
292
+ videoConstraints: !videoMuted,
293
+ audioConstraints: !audioMuted
236
294
  });
295
+ this.localStream = acquired.stream;
296
+ console.log(
297
+ "[LOCAL MEDIA]",
298
+ this.localStream.getTracks().map((t) => ({
299
+ kind: t.kind,
300
+ enabled: t.enabled,
301
+ ready: t.readyState
302
+ }))
303
+ );
304
+ camEnabled = acquired.camEnabled && !videoMuted;
305
+ micEnabled = acquired.micEnabled && !audioMuted;
237
306
  }
238
- this.localStream.getAudioTracks().forEach((t) => t.enabled = !audioMuted);
239
- this.localStream.getVideoTracks().forEach((t) => t.enabled = !videoMuted);
307
+ this.localStream.getAudioTracks().forEach((t) => t.enabled = micEnabled);
308
+ this.localStream.getVideoTracks().forEach((t) => t.enabled = camEnabled);
309
+ const cameraTrack = this.localStream.getVideoTracks()[0] || void 0;
310
+ const audioTrack = this.localStream.getAudioTracks()[0] || void 0;
240
311
  this.state.updateLocalParticipant({
241
312
  id: this.myId,
242
313
  name: this.participantName,
243
314
  media: {
244
315
  stream: this.localStream,
245
- micEnabled: !audioMuted,
246
- camEnabled: !videoMuted,
316
+ cameraTrack,
317
+ audioTrack,
318
+ micEnabled,
319
+ camEnabled,
247
320
  isScreenSharing: false
248
321
  }
249
322
  });
@@ -257,9 +330,39 @@ var VideoSDKCore = class {
257
330
  iceServers: this.iceServers,
258
331
  iceTransportPolicy: this.iceTransportPolicy
259
332
  });
333
+ const tracksByKind = /* @__PURE__ */ new Map();
260
334
  this.localStream.getTracks().forEach((track) => {
261
- this.pubPC?.addTrack(track, this.localStream);
335
+ tracksByKind.set(track.kind, track);
262
336
  });
337
+ const audioTrack = this.localStream.getAudioTracks()[0];
338
+ if (audioTrack) {
339
+ console.log("[Publisher] Adding audio track:", {
340
+ kind: audioTrack.kind,
341
+ id: audioTrack.id,
342
+ enabled: audioTrack.enabled,
343
+ state: audioTrack.readyState
344
+ });
345
+ try {
346
+ this.pubPC?.addTrack(audioTrack, this.localStream);
347
+ } catch (e) {
348
+ console.error("[Publisher] Failed to add audio track:", e);
349
+ }
350
+ }
351
+ const videoTrack = this.localStream.getVideoTracks()[0];
352
+ if (videoTrack) {
353
+ console.log("[Publisher] Adding video track:", {
354
+ kind: videoTrack.kind,
355
+ id: videoTrack.id,
356
+ enabled: videoTrack.enabled,
357
+ // Can be false, that's OK
358
+ state: videoTrack.readyState
359
+ });
360
+ try {
361
+ this.pubPC?.addTrack(videoTrack, this.localStream);
362
+ } catch (e) {
363
+ console.error("[Publisher] Failed to add video track:", e);
364
+ }
365
+ }
263
366
  this.pubPC.onicecandidate = (e) => {
264
367
  if (e.candidate) {
265
368
  this.send({
@@ -270,8 +373,13 @@ var VideoSDKCore = class {
270
373
  }
271
374
  };
272
375
  this.pubPC.onconnectionstatechange = () => {
273
- console.log(`[SFU Publisher PC State]`, this.pubPC?.connectionState);
376
+ console.log("[Publisher PC State]", {
377
+ connection: this.pubPC?.connectionState,
378
+ ice: this.pubPC?.iceConnectionState,
379
+ signaling: this.pubPC?.signalingState
380
+ });
274
381
  if (this.pubPC?.connectionState === "failed") {
382
+ console.warn("[Publisher] Connection failed, restarting ICE");
275
383
  this.restartPublisherIce();
276
384
  }
277
385
  };
@@ -291,44 +399,70 @@ var VideoSDKCore = class {
291
399
  }
292
400
  };
293
401
  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
- }
402
+ console.log("[SFU ontrack Event]", {
403
+ kind: event.track.kind,
404
+ id: event.track.id,
405
+ mid: event.transceiver.mid,
406
+ streams: event.streams.length
407
+ });
408
+ const stream = event.streams[0] || new MediaStream([event.track]);
409
+ const mid = event.transceiver.mid;
410
+ if (!mid) {
411
+ console.warn("[Subscriber] Track received without MID", event.track);
412
+ return;
302
413
  }
303
- if (!matchedParticipant) {
304
- console.warn(
305
- `[SFU ontrack] Dynamic track received for stream ${streamId}`
306
- );
414
+ console.log("[Subscriber] Looking for descriptor with MID:", mid, {
415
+ available: [...this.pendingTracks.keys()]
416
+ });
417
+ const descriptor = this.pendingTracks.get(mid);
418
+ if (!descriptor) {
419
+ console.warn("[Subscriber] No descriptor found for MID:", mid);
420
+ console.log("[Subscriber] Pending tracks:", [
421
+ ...this.pendingTracks.entries()
422
+ ]);
307
423
  return;
308
424
  }
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);
425
+ console.log("[Subscriber] Found descriptor:", descriptor);
426
+ this.pendingTracks.delete(mid);
427
+ switch (descriptor.source) {
428
+ case "camera":
429
+ console.log(
430
+ "[Subscriber] Updating camera for:",
431
+ descriptor.publisher_id
432
+ );
433
+ this.state.updateParticipantMedia(descriptor.publisher_id, {
434
+ stream,
435
+ cameraTrack: event.track
436
+ });
437
+ break;
438
+ case "audio":
439
+ console.log(
440
+ "[Subscriber] Updating audio for:",
441
+ descriptor.publisher_id
442
+ );
443
+ this.state.updateParticipantMedia(descriptor.publisher_id, {
444
+ stream,
445
+ audioTrack: event.track
446
+ });
447
+ break;
448
+ case "screen":
449
+ console.log(
450
+ "[Subscriber] Updating screen for:",
451
+ descriptor.publisher_id
452
+ );
453
+ this.state.updateParticipantMedia(descriptor.publisher_id, {
454
+ screenStream: stream,
455
+ screenTrack: event.track,
456
+ isScreenSharing: true
457
+ });
458
+ break;
328
459
  }
329
460
  };
330
461
  this.subPC.onconnectionstatechange = () => {
331
- console.log(`[SFU Subscriber PC State]`, this.subPC?.connectionState);
462
+ console.log("[SFU Subscriber PC State]", this.subPC?.connectionState);
463
+ if (this.subPC?.connectionState === "failed") {
464
+ console.warn("Subscriber connection failed");
465
+ }
332
466
  };
333
467
  }
334
468
  // ---------------- WEBSOCKET CONNECTION & SIGNALING ----------------
@@ -391,13 +525,26 @@ var VideoSDKCore = class {
391
525
  this.setupPublisherPC();
392
526
  this.setupSubscriberPC();
393
527
  await this.createPublisherOffer();
528
+ const media = this.state.localParticipant?.media;
529
+ if (media) {
530
+ this.send({
531
+ type: "MEDIA_STATE",
532
+ kind: "audio",
533
+ enabled: !!media.micEnabled
534
+ });
535
+ this.send({
536
+ type: "MEDIA_STATE",
537
+ kind: "video",
538
+ enabled: !!media.camEnabled
539
+ });
540
+ }
394
541
  this.startHeartbeat();
395
542
  this.joinResolver?.();
396
543
  this.joinResolver = void 0;
397
544
  this.joinRejecter = void 0;
398
545
  break;
399
546
  }
400
- case "SFU_PUB_ANSWER": {
547
+ case "PUB_ANSWER": {
401
548
  if (this.pubPC) {
402
549
  await this.pubPC.setRemoteDescription({
403
550
  type: "answer",
@@ -406,20 +553,27 @@ var VideoSDKCore = class {
406
553
  }
407
554
  break;
408
555
  }
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
- });
556
+ case "SUB_OFFER": {
557
+ if (!this.subPC) {
558
+ console.warn("Subscriber PC not ready");
559
+ return;
560
+ }
561
+ const descriptor = msg.track;
562
+ const mid = descriptor.mid;
563
+ console.log(
564
+ "[Signaling] Received SUB_OFFER with descriptor:",
565
+ descriptor
566
+ );
567
+ if (mid) {
568
+ this.pendingTracks.set(mid, descriptor);
569
+ console.log("[Signaling] Stored pending track for MID:", mid);
570
+ }
571
+ if (this.subscriberNegotiating) {
572
+ console.warn("Subscriber negotiating, queueing offer");
573
+ this.subscriberOfferQueue.push(msg);
574
+ return;
422
575
  }
576
+ await this.handleSubscriberOffer(msg);
423
577
  break;
424
578
  }
425
579
  case "PUB_ICE": {
@@ -566,6 +720,31 @@ var VideoSDKCore = class {
566
720
  }
567
721
  }
568
722
  }
723
+ async handleSubscriberOffer(msg) {
724
+ if (!this.subPC) return;
725
+ try {
726
+ this.subscriberNegotiating = true;
727
+ await this.subPC.setRemoteDescription({
728
+ type: "offer",
729
+ sdp: msg.payload
730
+ });
731
+ const answer = await this.subPC.createAnswer();
732
+ await this.subPC.setLocalDescription(answer);
733
+ this.send({
734
+ type: "SUB_ANSWER",
735
+ payload: answer.sdp,
736
+ user_id: this.myId
737
+ });
738
+ } catch (err) {
739
+ console.error("[SUB OFFER ERROR]", err);
740
+ } finally {
741
+ this.subscriberNegotiating = false;
742
+ const next = this.subscriberOfferQueue.shift();
743
+ if (next) {
744
+ await this.handleSubscriberOffer(next);
745
+ }
746
+ }
747
+ }
569
748
  // ---------------- PUBLISHER RENEGOTIATION ----------------
570
749
  async createPublisherOffer() {
571
750
  if (!this.pubPC) return;
@@ -618,6 +797,9 @@ var VideoSDKCore = class {
618
797
  });
619
798
  this.isScreenSharing = true;
620
799
  const screenTrack = this.screenStream.getVideoTracks()[0];
800
+ Object.defineProperty(screenTrack, "contentHint", {
801
+ value: "detail"
802
+ });
621
803
  if (this.pubPC) {
622
804
  this.screenSender = this.pubPC.addTrack(screenTrack, this.screenStream);
623
805
  await this.createPublisherOffer();
@@ -637,7 +819,6 @@ var VideoSDKCore = class {
637
819
  type: "SCREEN_SHARE_START",
638
820
  sender: this.myId,
639
821
  room_id: this.room.id,
640
- camera_id: this.localStream?.id.replace(/[{}]/g, ""),
641
822
  stream_id: this.screenStream.id.replace(/[{}]/g, "")
642
823
  });
643
824
  return this.screenStream;
@@ -653,17 +834,13 @@ var VideoSDKCore = class {
653
834
  throw err;
654
835
  }
655
836
  }
656
- stopScreenShare() {
837
+ async stopScreenShare() {
657
838
  if (!this.screenStream) return;
658
839
  this.screenStream.getTracks().forEach((t) => t.stop());
659
840
  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
- }
841
+ this.pubPC.removeTrack(this.screenSender);
666
842
  this.screenSender = null;
843
+ await this.createPublisherOffer();
667
844
  }
668
845
  this.screenStream = null;
669
846
  this.isScreenSharing = false;