@afosecure/meetingsdk 1.3.0 → 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);
@@ -516,7 +519,6 @@ var VideoSDKCore = class {
516
519
  if (msg.presenterId) {
517
520
  this.state.setPresenterId(msg.presenterId);
518
521
  this.events.onScreenShareStarted?.(msg.presenterId, null);
519
- this.state.setPresenterId(msg.presenterId);
520
522
  }
521
523
  for (const p of msg.participants || []) {
522
524
  if (!p?.id || p.id === this.myId) continue;
@@ -599,12 +601,10 @@ var VideoSDKCore = class {
599
601
  });
600
602
  break;
601
603
  }
602
- // ============ NEW: HANDLE JOIN_APPROVED WITH RECONNECT ============
603
604
  case "JOIN_APPROVED": {
604
605
  await this.handleJoinApproved(msg);
605
606
  break;
606
607
  }
607
- // ============ END: JOIN_APPROVED ============
608
608
  case "JOIN_REJECTED": {
609
609
  const decision = "rejected";
610
610
  console.log("JOIN_REJECTED - user not allowed to join");
@@ -657,8 +657,7 @@ var VideoSDKCore = class {
657
657
  if (!this.state.presenterId) {
658
658
  this.state.setPresenterId(peerId2);
659
659
  }
660
- const screenStream = this.state.getParticipant(peerId2)?.media?.screenStream;
661
- this.events.onScreenShareStarted?.(peerId2, screenStream || null);
660
+ this.events.onScreenShareStarted?.(peerId2, null);
662
661
  break;
663
662
  }
664
663
  case "SCREEN_SHARE_STOP": {
@@ -685,8 +684,17 @@ var VideoSDKCore = class {
685
684
  }
686
685
  }
687
686
  }
688
- // ---------------- PEER ----------------
689
- 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) {
690
698
  if (!this.localStream) throw new Error("No local stream");
691
699
  if (!this.iceServers || this.iceServers.length === 0) {
692
700
  throw new Error(
@@ -694,7 +702,9 @@ var VideoSDKCore = class {
694
702
  );
695
703
  }
696
704
  console.log(
697
- "Adding tracks",
705
+ "Creating peer connection for",
706
+ id,
707
+ "with tracks:",
698
708
  this.localStream.getTracks().map((t) => ({
699
709
  kind: t.kind,
700
710
  enabled: t.enabled,
@@ -704,20 +714,52 @@ var VideoSDKCore = class {
704
714
  const pc = new RTCPeerConnection({
705
715
  iceServers: this.iceServers
706
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
+ };
707
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
+ );
708
752
  const incomingStream = event.streams?.[0] || new MediaStream([event.track]);
709
- const participant = this.state.getParticipant(id);
710
- const isScreenStream = participant?.media?.isScreenSharing && incomingStream.id === participant?.media?.remoteScreenStreamId;
711
753
  if (event.track.muted) {
712
754
  event.track.onunmute = () => {
713
- console.log(`${event.track.kind} track unmuted for ${id}`);
755
+ console.log(`[ontrack] ${event.track.kind} track unmuted for ${id}`);
714
756
  };
715
757
  }
716
- if (isScreenStream) {
717
- 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];
718
760
  this.state.updateParticipantMedia(id, {
719
761
  screenStream: incomingStream,
720
- screenTrack: videoTrack,
762
+ screenTrack: videoTrack2,
721
763
  isScreenSharing: true
722
764
  });
723
765
  if (!this.state.presenterId) {
@@ -743,29 +785,41 @@ var VideoSDKCore = class {
743
785
  });
744
786
  };
745
787
  pc.oniceconnectionstatechange = () => {
746
- console.log(`ICE Connection State: ${pc.iceConnectionState}`);
788
+ console.log(`[ICE Connection] ${id}: ${pc.iceConnectionState}`);
747
789
  };
748
790
  pc.onconnectionstatechange = () => {
791
+ console.log(`[Connection] ${id}: ${pc.connectionState}`);
749
792
  if (pc.connectionState === "failed") {
750
793
  try {
751
794
  pc.restartIce();
752
- } catch {
795
+ } catch (e) {
796
+ console.warn("Failed to restart ICE:", e);
753
797
  }
754
798
  }
755
799
  };
756
- this.localStream.getTracks().forEach((track) => {
757
- pc.addTrack(track, this.localStream);
758
- });
759
- if (this.isScreenSharing && this.screenStream) {
760
- this.screenSenders[id] = [];
761
- this.screenStream.getTracks().forEach((track) => {
762
- const sender = pc.addTrack(track, this.screenStream);
763
- this.screenSenders[id].push(sender);
764
- });
765
- }
766
800
  return pc;
767
801
  }
768
- // ---------------- 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
769
823
  async createOffer(id, isRenegotiation = false) {
770
824
  if (!isRenegotiation && !this.shouldInitiate(id)) {
771
825
  console.debug(
@@ -783,12 +837,13 @@ var VideoSDKCore = class {
783
837
  this.initiators.add(id);
784
838
  }
785
839
  if (!this.peers[id]) {
786
- this.peers[id] = this.createPeer(id);
840
+ this.peers[id] = await this.createPeer(id);
787
841
  }
788
842
  const pc = this.peers[id];
789
843
  try {
790
844
  const offer = await pc.createOffer();
791
845
  await pc.setLocalDescription(offer);
846
+ this.captureScreenMid(id);
792
847
  this.send({
793
848
  type: "OFFER",
794
849
  payload: offer.sdp,
@@ -798,12 +853,18 @@ var VideoSDKCore = class {
798
853
  console.debug(`[Offer] Sent to ${id}`);
799
854
  } catch (err) {
800
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
+ );
801
862
  }
802
863
  }
803
864
  shouldInitiate(peerId) {
804
865
  return this.myId < peerId;
805
866
  }
806
- // ---------------- ANSWER ----------------
867
+ // ANSWER
807
868
  async handleOffer(sdp, id) {
808
869
  if (!this.iceServers || this.iceServers.length === 0) {
809
870
  console.warn("[Offer] Waiting for iceServers, queuing offer from", id);
@@ -811,7 +872,7 @@ var VideoSDKCore = class {
811
872
  return;
812
873
  }
813
874
  if (!this.peers[id]) {
814
- this.peers[id] = this.createPeer(id);
875
+ this.peers[id] = await this.createPeer(id);
815
876
  }
816
877
  const pc = this.peers[id];
817
878
  try {
@@ -827,8 +888,9 @@ var VideoSDKCore = class {
827
888
  );
828
889
  pc.close();
829
890
  delete this.peers[id];
891
+ delete this.peerTransceivers[id];
830
892
  this.initiators.delete(id);
831
- this.peers[id] = this.createPeer(id);
893
+ this.peers[id] = await this.createPeer(id);
832
894
  }
833
895
  }
834
896
  if (this.peers[id].signalingState !== "stable" && this.peers[id].signalingState !== "have-local-offer") {
@@ -841,6 +903,7 @@ var VideoSDKCore = class {
841
903
  type: "offer",
842
904
  sdp
843
905
  });
906
+ this.captureScreenMid(id);
844
907
  const pending = this.pendingIceCandidates[id] || [];
845
908
  for (const candidate of pending) {
846
909
  try {
@@ -870,18 +933,26 @@ var VideoSDKCore = class {
870
933
  );
871
934
  }
872
935
  }
873
- // ---------------- CLEANUP ----------------
936
+ // CLEANUP
874
937
  closePeer(id) {
875
938
  const pc = this.peers[id];
876
939
  if (!pc) return;
877
940
  pc.ontrack = null;
878
941
  pc.onicecandidate = null;
879
942
  pc.onconnectionstatechange = null;
943
+ pc.oniceconnectionstatechange = null;
880
944
  pc.close();
881
945
  delete this.peers[id];
946
+ delete this.peerTransceivers[id];
882
947
  this.initiators.delete(id);
883
948
  this.state.removeParticipant(id);
884
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
+ */
885
956
  async startScreenShare() {
886
957
  try {
887
958
  if (this.state.presenterId && this.state.presenterId !== this.myId) {
@@ -892,34 +963,55 @@ var VideoSDKCore = class {
892
963
  }
893
964
  this.screenStream = await navigator.mediaDevices.getDisplayMedia({
894
965
  video: true
895
- // audio: true,
896
966
  });
967
+ const screenTrack = this.screenStream.getVideoTracks()[0];
968
+ if (!screenTrack) {
969
+ throw new Error("No video track in screen stream");
970
+ }
897
971
  this.isScreenSharing = true;
898
972
  this.state.updateLocalParticipant({
899
973
  media: {
900
974
  isScreenSharing: true,
901
975
  screenStream: this.screenStream,
902
- screenTrack: this.screenStream.getVideoTracks()[0]
976
+ screenTrack
903
977
  }
904
978
  });
905
979
  this.state.setPresenterId(this.myId);
906
- this.screenStream.getVideoTracks()[0].onended = () => {
980
+ screenTrack.onended = () => {
981
+ console.log("[Screen Share] User stopped via browser button");
907
982
  this.stopScreenShare();
908
983
  };
909
- Object.entries(this.peers).forEach(([peerId, pc]) => {
910
- this.screenSenders[peerId] = [];
911
- this.screenStream.getTracks().forEach((track) => {
912
- const sender = pc.addTrack(track, this.screenStream);
913
- this.screenSenders[peerId].push(sender);
914
- });
915
- this.createOffer(peerId, true);
916
- });
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
+ }
917
1008
  this.send({
918
1009
  type: "SCREEN_SHARE_START",
919
1010
  sender: this.myId,
920
1011
  room_id: this.room.id,
921
1012
  stream_id: this.screenStream.id.replace(/[{}]/g, "")
922
1013
  });
1014
+ console.log("[Screen Share] Started successfully");
923
1015
  return this.screenStream;
924
1016
  } catch (err) {
925
1017
  this.emitError(
@@ -933,21 +1025,29 @@ var VideoSDKCore = class {
933
1025
  throw err;
934
1026
  }
935
1027
  }
936
- stopScreenShare() {
1028
+ async stopScreenShare() {
937
1029
  if (!this.screenStream) return;
1030
+ console.log("[Screen Share] Stopping...");
938
1031
  this.screenStream.getTracks().forEach((t) => t.stop());
939
- Object.entries(this.peers).forEach(([peerId, pc]) => {
940
- const senders = this.screenSenders[peerId] || [];
941
- senders.forEach((sender) => {
942
- try {
943
- pc.removeTrack(sender);
944
- } catch (err) {
945
- 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);
946
1043
  }
947
- });
948
- delete this.screenSenders[peerId];
949
- this.createOffer(peerId, true);
950
- });
1044
+ } catch (err) {
1045
+ console.error(
1046
+ `[Screen Share] Failed to clear transceiver for ${peerId}:`,
1047
+ err
1048
+ );
1049
+ }
1050
+ }
951
1051
  this.screenStream = null;
952
1052
  this.isScreenSharing = false;
953
1053
  this.state.updateLocalParticipant({
@@ -965,7 +1065,9 @@ var VideoSDKCore = class {
965
1065
  sender: this.myId,
966
1066
  room_id: this.room.id
967
1067
  });
1068
+ console.log("[Screen Share] Stopped");
968
1069
  }
1070
+ // CHAT
969
1071
  sendChatMessage(payload) {
970
1072
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
971
1073
  console.warn("WS not connected");
@@ -998,11 +1100,13 @@ var VideoSDKCore = class {
998
1100
  client_ts: Date.now()
999
1101
  });
1000
1102
  }
1103
+ // DISCONNECT
1001
1104
  disconnect() {
1002
1105
  this.intentionalDisconnect = true;
1003
1106
  this.stopScreenShare();
1004
1107
  Object.values(this.peers).forEach((pc) => pc.close());
1005
1108
  this.peers = {};
1109
+ this.peerTransceivers = {};
1006
1110
  this.initiators.clear();
1007
1111
  this.stopHeartbeat();
1008
1112
  if (this.ws?.readyState === WebSocket.OPEN) {