@afosecure/meetingsdk 1.2.9 → 1.3.1

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.d.mts CHANGED
@@ -130,7 +130,7 @@ declare class VideoSDKCore {
130
130
  private localStream;
131
131
  private screenStream;
132
132
  private isScreenSharing;
133
- private screenSenders;
133
+ private peerTransceivers;
134
134
  private pingInterval;
135
135
  private pendingIceCandidates;
136
136
  private pendingOffers;
@@ -159,13 +159,32 @@ declare class VideoSDKCore {
159
159
  private reset;
160
160
  private handleJoinApproved;
161
161
  private handle;
162
+ /**
163
+ * Create a peer connection with pre-established transceiver layout:
164
+ * - Audio transceiver (sendrecv)
165
+ * - Camera video transceiver (sendrecv)
166
+ * - Screen video transceiver (initially recvonly, becomes sendrecv when sharing)
167
+ *
168
+ * This fixed layout ensures late joiners get the screen transceiver m-line
169
+ * negotiated from the very first offer, even if no one is sharing yet.
170
+ */
162
171
  private createPeer;
172
+ /**
173
+ * Capture the screen transceiver's MID after SDP negotiation completes.
174
+ * The MID is assigned during negotiation and is stable for the life of the connection.
175
+ */
176
+ private captureScreenMid;
163
177
  private createOffer;
164
178
  private shouldInitiate;
165
179
  private handleOffer;
166
180
  private closePeer;
181
+ /**
182
+ * Start screen sharing using replaceTrack on the pre-established screen transceiver.
183
+ * No need to add/remove tracks, no renegotiation needed (transceiver already in SDP).
184
+ * Just swap the track and update direction if needed.
185
+ */
167
186
  startScreenShare(): Promise<MediaStream>;
168
- stopScreenShare(): void;
187
+ stopScreenShare(): Promise<void>;
169
188
  sendChatMessage(payload: ChatInput): void;
170
189
  disconnect(): void;
171
190
  private flushIce;
package/dist/index.d.ts CHANGED
@@ -130,7 +130,7 @@ declare class VideoSDKCore {
130
130
  private localStream;
131
131
  private screenStream;
132
132
  private isScreenSharing;
133
- private screenSenders;
133
+ private peerTransceivers;
134
134
  private pingInterval;
135
135
  private pendingIceCandidates;
136
136
  private pendingOffers;
@@ -159,13 +159,32 @@ declare class VideoSDKCore {
159
159
  private reset;
160
160
  private handleJoinApproved;
161
161
  private handle;
162
+ /**
163
+ * Create a peer connection with pre-established transceiver layout:
164
+ * - Audio transceiver (sendrecv)
165
+ * - Camera video transceiver (sendrecv)
166
+ * - Screen video transceiver (initially recvonly, becomes sendrecv when sharing)
167
+ *
168
+ * This fixed layout ensures late joiners get the screen transceiver m-line
169
+ * negotiated from the very first offer, even if no one is sharing yet.
170
+ */
162
171
  private createPeer;
172
+ /**
173
+ * Capture the screen transceiver's MID after SDP negotiation completes.
174
+ * The MID is assigned during negotiation and is stable for the life of the connection.
175
+ */
176
+ private captureScreenMid;
163
177
  private createOffer;
164
178
  private shouldInitiate;
165
179
  private handleOffer;
166
180
  private closePeer;
181
+ /**
182
+ * Start screen sharing using replaceTrack on the pre-established screen transceiver.
183
+ * No need to add/remove tracks, no renegotiation needed (transceiver already in SDP).
184
+ * Just swap the track and update direction if needed.
185
+ */
167
186
  startScreenShare(): Promise<MediaStream>;
168
- stopScreenShare(): void;
187
+ stopScreenShare(): Promise<void>;
169
188
  sendChatMessage(payload: ChatInput): void;
170
189
  disconnect(): void;
171
190
  private flushIce;
package/dist/index.js CHANGED
@@ -210,7 +210,8 @@ var VideoSDKCore = class {
210
210
  this.localStream = null;
211
211
  this.screenStream = null;
212
212
  this.isScreenSharing = false;
213
- this.screenSenders = {};
213
+ // Transceiver-based tracking: maps peerId -> { cameraTransceiver, screenTransceiver, screenMid }
214
+ this.peerTransceivers = {};
214
215
  this.pingInterval = null;
215
216
  this.pendingIceCandidates = {};
216
217
  this.pendingOffers = {};
@@ -239,7 +240,7 @@ var VideoSDKCore = class {
239
240
  this.joinRejecter = void 0;
240
241
  console.error("[MeetingSDK Error]", err);
241
242
  }
242
- // ---------------- STREAM ----------------
243
+ // STREAM
243
244
  async initLocal(video, name) {
244
245
  this.participantName = name;
245
246
  try {
@@ -276,7 +277,7 @@ var VideoSDKCore = class {
276
277
  throw err;
277
278
  }
278
279
  }
279
- // ---------------- CONNECT ----------------
280
+ // CONNECT
280
281
  async connect(roomId, name) {
281
282
  this.room.id = roomId;
282
283
  this.reset();
@@ -422,10 +423,11 @@ var VideoSDKCore = class {
422
423
  this.pingInterval = null;
423
424
  }
424
425
  }
425
- // ---------------- RESET ----------------
426
+ // RESET
426
427
  reset() {
427
428
  Object.values(this.peers).forEach((pc) => pc.close());
428
429
  this.peers = {};
430
+ this.peerTransceivers = {};
429
431
  this.initiators.clear();
430
432
  this.pendingIceCandidates = {};
431
433
  this.state.resetRemoteState();
@@ -480,6 +482,7 @@ var VideoSDKCore = class {
480
482
  type: "answer",
481
483
  sdp: msg.payload
482
484
  });
485
+ this.captureScreenMid(msg.sender);
483
486
  await this.flushIce(msg.sender, pc);
484
487
  } catch (err) {
485
488
  console.error("[Signaling] Failed to apply answer:", err);
@@ -525,11 +528,14 @@ var VideoSDKCore = class {
525
528
  console.log(
526
529
  `[Existing Users] ${p.name} is sharing screen (stream: ${p.remoteScreenStreamId})`
527
530
  );
528
- this.state.setPresenterId(p.presenterId || null);
531
+ this.state.setPresenterId(p.id);
529
532
  this.state.updateParticipantMedia(p.id, {
530
533
  isScreenSharing: true,
531
534
  remoteScreenStreamId: p.remoteScreenStreamId
532
535
  });
536
+ console.log(
537
+ `[EXISTING_USERS] Pre-seeded screen state for ${p.id}, waiting for ontrack`
538
+ );
533
539
  }
534
540
  if (this.shouldInitiate(p.id)) {
535
541
  await this.createOffer(p.id);
@@ -595,12 +601,10 @@ var VideoSDKCore = class {
595
601
  });
596
602
  break;
597
603
  }
598
- // ============ NEW: HANDLE JOIN_APPROVED WITH RECONNECT ============
599
604
  case "JOIN_APPROVED": {
600
605
  await this.handleJoinApproved(msg);
601
606
  break;
602
607
  }
603
- // ============ END: JOIN_APPROVED ============
604
608
  case "JOIN_REJECTED": {
605
609
  const decision = "rejected";
606
610
  console.log("JOIN_REJECTED - user not allowed to join");
@@ -653,8 +657,7 @@ var VideoSDKCore = class {
653
657
  if (!this.state.presenterId) {
654
658
  this.state.setPresenterId(peerId2);
655
659
  }
656
- const screenStream = this.state.getParticipant(peerId2)?.media?.screenStream;
657
- this.events.onScreenShareStarted?.(peerId2, screenStream || null);
660
+ this.events.onScreenShareStarted?.(peerId2, null);
658
661
  break;
659
662
  }
660
663
  case "SCREEN_SHARE_STOP": {
@@ -681,8 +684,17 @@ var VideoSDKCore = class {
681
684
  }
682
685
  }
683
686
  }
684
- // ---------------- PEER ----------------
685
- createPeer(id) {
687
+ // PEER
688
+ /**
689
+ * Create a peer connection with pre-established transceiver layout:
690
+ * - Audio transceiver (sendrecv)
691
+ * - Camera video transceiver (sendrecv)
692
+ * - Screen video transceiver (initially recvonly, becomes sendrecv when sharing)
693
+ *
694
+ * This fixed layout ensures late joiners get the screen transceiver m-line
695
+ * negotiated from the very first offer, even if no one is sharing yet.
696
+ */
697
+ async createPeer(id) {
686
698
  if (!this.localStream) throw new Error("No local stream");
687
699
  if (!this.iceServers || this.iceServers.length === 0) {
688
700
  throw new Error(
@@ -690,7 +702,9 @@ var VideoSDKCore = class {
690
702
  );
691
703
  }
692
704
  console.log(
693
- "Adding tracks",
705
+ "Creating peer connection for",
706
+ id,
707
+ "with tracks:",
694
708
  this.localStream.getTracks().map((t) => ({
695
709
  kind: t.kind,
696
710
  enabled: t.enabled,
@@ -700,20 +714,52 @@ var VideoSDKCore = class {
700
714
  const pc = new RTCPeerConnection({
701
715
  iceServers: this.iceServers
702
716
  });
717
+ const audioTransceiver = pc.addTransceiver("audio", {
718
+ direction: "sendrecv"
719
+ });
720
+ const audioTrack = this.localStream.getAudioTracks()[0];
721
+ if (audioTrack) {
722
+ await audioTransceiver.sender.replaceTrack(audioTrack);
723
+ }
724
+ const cameraTransceiver = pc.addTransceiver("video", {
725
+ direction: "sendrecv"
726
+ });
727
+ const videoTrack = this.localStream.getVideoTracks()[0];
728
+ if (videoTrack) {
729
+ await cameraTransceiver.sender.replaceTrack(videoTrack);
730
+ }
731
+ const screenTransceiver = pc.addTransceiver("video", {
732
+ direction: this.isScreenSharing ? "sendrecv" : "recvonly"
733
+ });
734
+ if (this.isScreenSharing && this.screenStream) {
735
+ const screenTrack = this.screenStream.getVideoTracks()[0];
736
+ if (screenTrack) {
737
+ await screenTransceiver.sender.replaceTrack(screenTrack);
738
+ }
739
+ }
740
+ this.peerTransceivers[id] = {
741
+ cameraTransceiver,
742
+ screenTransceiver,
743
+ screenMid: null
744
+ // will be populated after negotiation
745
+ };
703
746
  pc.ontrack = (event) => {
747
+ const transceiver = event.transceiver;
748
+ const isScreenTrack = transceiver.mid === this.peerTransceivers[id]?.screenMid;
749
+ console.log(
750
+ `[ontrack] ${id}: kind=${event.track.kind}, mid=${transceiver.mid}, isScreen=${isScreenTrack}`
751
+ );
704
752
  const incomingStream = event.streams?.[0] || new MediaStream([event.track]);
705
- const participant = this.state.getParticipant(id);
706
- const isScreenStream = incomingStream.id === participant?.media?.remoteScreenStreamId;
707
753
  if (event.track.muted) {
708
754
  event.track.onunmute = () => {
709
- console.log(`${event.track.kind} track unmuted for ${id}`);
755
+ console.log(`[ontrack] ${event.track.kind} track unmuted for ${id}`);
710
756
  };
711
757
  }
712
- if (isScreenStream) {
713
- const videoTrack = event.track.kind === "video" ? event.track : incomingStream.getVideoTracks()[0] || participant?.media?.screenTrack;
758
+ if (isScreenTrack) {
759
+ const videoTrack2 = event.track.kind === "video" ? event.track : incomingStream.getVideoTracks()[0];
714
760
  this.state.updateParticipantMedia(id, {
715
761
  screenStream: incomingStream,
716
- screenTrack: videoTrack,
762
+ screenTrack: videoTrack2,
717
763
  isScreenSharing: true
718
764
  });
719
765
  if (!this.state.presenterId) {
@@ -739,29 +785,41 @@ var VideoSDKCore = class {
739
785
  });
740
786
  };
741
787
  pc.oniceconnectionstatechange = () => {
742
- console.log(`ICE Connection State: ${pc.iceConnectionState}`);
788
+ console.log(`[ICE Connection] ${id}: ${pc.iceConnectionState}`);
743
789
  };
744
790
  pc.onconnectionstatechange = () => {
791
+ console.log(`[Connection] ${id}: ${pc.connectionState}`);
745
792
  if (pc.connectionState === "failed") {
746
793
  try {
747
794
  pc.restartIce();
748
- } catch {
795
+ } catch (e) {
796
+ console.warn("Failed to restart ICE:", e);
749
797
  }
750
798
  }
751
799
  };
752
- this.localStream.getTracks().forEach((track) => {
753
- pc.addTrack(track, this.localStream);
754
- });
755
- if (this.isScreenSharing && this.screenStream) {
756
- this.screenSenders[id] = [];
757
- this.screenStream.getTracks().forEach((track) => {
758
- const sender = pc.addTrack(track, this.screenStream);
759
- this.screenSenders[id].push(sender);
760
- });
761
- }
762
800
  return pc;
763
801
  }
764
- // ---------------- OFFER ----------------
802
+ /**
803
+ * Capture the screen transceiver's MID after SDP negotiation completes.
804
+ * The MID is assigned during negotiation and is stable for the life of the connection.
805
+ */
806
+ captureScreenMid(peerId) {
807
+ const pc = this.peers[peerId];
808
+ if (!pc) return;
809
+ const transceivers = pc.getTransceivers();
810
+ const screenTransceiver = this.peerTransceivers[peerId]?.screenTransceiver;
811
+ if (!screenTransceiver) return;
812
+ const negotiatedTransceiver = transceivers.find(
813
+ (t) => t === screenTransceiver
814
+ );
815
+ if (negotiatedTransceiver?.mid) {
816
+ this.peerTransceivers[peerId].screenMid = negotiatedTransceiver.mid;
817
+ console.log(
818
+ `[Negotiation] Captured screenMid for ${peerId}: ${negotiatedTransceiver.mid}`
819
+ );
820
+ }
821
+ }
822
+ // OFFER
765
823
  async createOffer(id, isRenegotiation = false) {
766
824
  if (!isRenegotiation && !this.shouldInitiate(id)) {
767
825
  console.debug(
@@ -779,12 +837,13 @@ var VideoSDKCore = class {
779
837
  this.initiators.add(id);
780
838
  }
781
839
  if (!this.peers[id]) {
782
- this.peers[id] = this.createPeer(id);
840
+ this.peers[id] = await this.createPeer(id);
783
841
  }
784
842
  const pc = this.peers[id];
785
843
  try {
786
844
  const offer = await pc.createOffer();
787
845
  await pc.setLocalDescription(offer);
846
+ this.captureScreenMid(id);
788
847
  this.send({
789
848
  type: "OFFER",
790
849
  payload: offer.sdp,
@@ -794,12 +853,18 @@ var VideoSDKCore = class {
794
853
  console.debug(`[Offer] Sent to ${id}`);
795
854
  } catch (err) {
796
855
  console.error(`[Offer] Failed for ${id}:`, err);
856
+ this.emitError(
857
+ "OFFER_CREATION_FAILED",
858
+ `Failed to create offer for ${id}`,
859
+ err,
860
+ true
861
+ );
797
862
  }
798
863
  }
799
864
  shouldInitiate(peerId) {
800
865
  return this.myId < peerId;
801
866
  }
802
- // ---------------- ANSWER ----------------
867
+ // ANSWER
803
868
  async handleOffer(sdp, id) {
804
869
  if (!this.iceServers || this.iceServers.length === 0) {
805
870
  console.warn("[Offer] Waiting for iceServers, queuing offer from", id);
@@ -807,7 +872,7 @@ var VideoSDKCore = class {
807
872
  return;
808
873
  }
809
874
  if (!this.peers[id]) {
810
- this.peers[id] = this.createPeer(id);
875
+ this.peers[id] = await this.createPeer(id);
811
876
  }
812
877
  const pc = this.peers[id];
813
878
  try {
@@ -823,8 +888,9 @@ var VideoSDKCore = class {
823
888
  );
824
889
  pc.close();
825
890
  delete this.peers[id];
891
+ delete this.peerTransceivers[id];
826
892
  this.initiators.delete(id);
827
- this.peers[id] = this.createPeer(id);
893
+ this.peers[id] = await this.createPeer(id);
828
894
  }
829
895
  }
830
896
  if (this.peers[id].signalingState !== "stable" && this.peers[id].signalingState !== "have-local-offer") {
@@ -837,6 +903,7 @@ var VideoSDKCore = class {
837
903
  type: "offer",
838
904
  sdp
839
905
  });
906
+ this.captureScreenMid(id);
840
907
  const pending = this.pendingIceCandidates[id] || [];
841
908
  for (const candidate of pending) {
842
909
  try {
@@ -866,18 +933,26 @@ var VideoSDKCore = class {
866
933
  );
867
934
  }
868
935
  }
869
- // ---------------- CLEANUP ----------------
936
+ // CLEANUP
870
937
  closePeer(id) {
871
938
  const pc = this.peers[id];
872
939
  if (!pc) return;
873
940
  pc.ontrack = null;
874
941
  pc.onicecandidate = null;
875
942
  pc.onconnectionstatechange = null;
943
+ pc.oniceconnectionstatechange = null;
876
944
  pc.close();
877
945
  delete this.peers[id];
946
+ delete this.peerTransceivers[id];
878
947
  this.initiators.delete(id);
879
948
  this.state.removeParticipant(id);
880
949
  }
950
+ // SCREEN SHARE (TRANSCEIVER-BASED)
951
+ /**
952
+ * Start screen sharing using replaceTrack on the pre-established screen transceiver.
953
+ * No need to add/remove tracks, no renegotiation needed (transceiver already in SDP).
954
+ * Just swap the track and update direction if needed.
955
+ */
881
956
  async startScreenShare() {
882
957
  try {
883
958
  if (this.state.presenterId && this.state.presenterId !== this.myId) {
@@ -888,34 +963,55 @@ var VideoSDKCore = class {
888
963
  }
889
964
  this.screenStream = await navigator.mediaDevices.getDisplayMedia({
890
965
  video: true
891
- // audio: true,
892
966
  });
967
+ const screenTrack = this.screenStream.getVideoTracks()[0];
968
+ if (!screenTrack) {
969
+ throw new Error("No video track in screen stream");
970
+ }
893
971
  this.isScreenSharing = true;
894
972
  this.state.updateLocalParticipant({
895
973
  media: {
896
974
  isScreenSharing: true,
897
975
  screenStream: this.screenStream,
898
- screenTrack: this.screenStream.getVideoTracks()[0]
976
+ screenTrack
899
977
  }
900
978
  });
901
979
  this.state.setPresenterId(this.myId);
902
- this.screenStream.getVideoTracks()[0].onended = () => {
980
+ screenTrack.onended = () => {
981
+ console.log("[Screen Share] User stopped via browser button");
903
982
  this.stopScreenShare();
904
983
  };
905
- Object.entries(this.peers).forEach(([peerId, pc]) => {
906
- this.screenSenders[peerId] = [];
907
- this.screenStream.getTracks().forEach((track) => {
908
- const sender = pc.addTrack(track, this.screenStream);
909
- this.screenSenders[peerId].push(sender);
910
- });
911
- this.createOffer(peerId, true);
912
- });
984
+ for (const [peerId, pc] of Object.entries(this.peers)) {
985
+ const txInfo = this.peerTransceivers[peerId];
986
+ if (!txInfo) {
987
+ console.warn(
988
+ `[Screen Share] No transceiver info for ${peerId}, skipping`
989
+ );
990
+ continue;
991
+ }
992
+ try {
993
+ await txInfo.screenTransceiver.sender.replaceTrack(screenTrack);
994
+ if (txInfo.screenTransceiver.currentDirection === "recvonly") {
995
+ txInfo.screenTransceiver.direction = "sendrecv";
996
+ console.log(
997
+ `[Screen Share] Flipped ${peerId} screen transceiver to sendrecv`
998
+ );
999
+ await this.createOffer(peerId, true);
1000
+ }
1001
+ } catch (err) {
1002
+ console.error(
1003
+ `[Screen Share] Failed to update transceiver for ${peerId}:`,
1004
+ err
1005
+ );
1006
+ }
1007
+ }
913
1008
  this.send({
914
1009
  type: "SCREEN_SHARE_START",
915
1010
  sender: this.myId,
916
1011
  room_id: this.room.id,
917
1012
  stream_id: this.screenStream.id.replace(/[{}]/g, "")
918
1013
  });
1014
+ console.log("[Screen Share] Started successfully");
919
1015
  return this.screenStream;
920
1016
  } catch (err) {
921
1017
  this.emitError(
@@ -929,21 +1025,29 @@ var VideoSDKCore = class {
929
1025
  throw err;
930
1026
  }
931
1027
  }
932
- stopScreenShare() {
1028
+ async stopScreenShare() {
933
1029
  if (!this.screenStream) return;
1030
+ console.log("[Screen Share] Stopping...");
934
1031
  this.screenStream.getTracks().forEach((t) => t.stop());
935
- Object.entries(this.peers).forEach(([peerId, pc]) => {
936
- const senders = this.screenSenders[peerId] || [];
937
- senders.forEach((sender) => {
938
- try {
939
- pc.removeTrack(sender);
940
- } catch (err) {
941
- console.warn(err);
1032
+ for (const [peerId, pc] of Object.entries(this.peers)) {
1033
+ const txInfo = this.peerTransceivers[peerId];
1034
+ if (!txInfo) continue;
1035
+ try {
1036
+ await txInfo.screenTransceiver.sender.replaceTrack(null);
1037
+ if (txInfo.screenTransceiver.currentDirection === "sendrecv") {
1038
+ txInfo.screenTransceiver.direction = "recvonly";
1039
+ console.log(
1040
+ `[Screen Share] Flipped ${peerId} screen transceiver to recvonly`
1041
+ );
1042
+ await this.createOffer(peerId, true);
942
1043
  }
943
- });
944
- delete this.screenSenders[peerId];
945
- this.createOffer(peerId, true);
946
- });
1044
+ } catch (err) {
1045
+ console.error(
1046
+ `[Screen Share] Failed to clear transceiver for ${peerId}:`,
1047
+ err
1048
+ );
1049
+ }
1050
+ }
947
1051
  this.screenStream = null;
948
1052
  this.isScreenSharing = false;
949
1053
  this.state.updateLocalParticipant({
@@ -961,7 +1065,9 @@ var VideoSDKCore = class {
961
1065
  sender: this.myId,
962
1066
  room_id: this.room.id
963
1067
  });
1068
+ console.log("[Screen Share] Stopped");
964
1069
  }
1070
+ // CHAT
965
1071
  sendChatMessage(payload) {
966
1072
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
967
1073
  console.warn("WS not connected");
@@ -994,11 +1100,13 @@ var VideoSDKCore = class {
994
1100
  client_ts: Date.now()
995
1101
  });
996
1102
  }
1103
+ // DISCONNECT
997
1104
  disconnect() {
998
1105
  this.intentionalDisconnect = true;
999
1106
  this.stopScreenShare();
1000
1107
  Object.values(this.peers).forEach((pc) => pc.close());
1001
1108
  this.peers = {};
1109
+ this.peerTransceivers = {};
1002
1110
  this.initiators.clear();
1003
1111
  this.stopHeartbeat();
1004
1112
  if (this.ws?.readyState === WebSocket.OPEN) {