@afosecure/meetingsdk 1.3.0 → 1.3.2

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;
@@ -160,14 +160,27 @@ declare class VideoSDKCore {
160
160
  private handleJoinApproved;
161
161
  private handle;
162
162
  private createPeer;
163
+ /**
164
+ * Capture the screen transceiver's MID after SDP negotiation completes.
165
+ * The MID is assigned during negotiation and is stable for the life of the connection.
166
+ */
167
+ private captureScreenMid;
163
168
  private createOffer;
164
169
  private shouldInitiate;
165
170
  private handleOffer;
166
171
  private closePeer;
172
+ /**
173
+ * Start screen sharing using replaceTrack on the pre-established screen transceiver.
174
+ * No need to add/remove tracks, no renegotiation needed (transceiver already in SDP).
175
+ * Just swap the track and update direction if needed.
176
+ */
167
177
  startScreenShare(): Promise<MediaStream>;
168
- stopScreenShare(): void;
178
+ /**
179
+ * Stop screen sharing: clear the screen transceiver track and flip direction back to recvonly.
180
+ */
181
+ stopScreenShare(): Promise<void>;
169
182
  sendChatMessage(payload: ChatInput): void;
170
- disconnect(): void;
183
+ disconnect(): Promise<void>;
171
184
  private flushIce;
172
185
  private send;
173
186
  approveJoinRequest(requestId: string): void;
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;
@@ -160,14 +160,27 @@ declare class VideoSDKCore {
160
160
  private handleJoinApproved;
161
161
  private handle;
162
162
  private createPeer;
163
+ /**
164
+ * Capture the screen transceiver's MID after SDP negotiation completes.
165
+ * The MID is assigned during negotiation and is stable for the life of the connection.
166
+ */
167
+ private captureScreenMid;
163
168
  private createOffer;
164
169
  private shouldInitiate;
165
170
  private handleOffer;
166
171
  private closePeer;
172
+ /**
173
+ * Start screen sharing using replaceTrack on the pre-established screen transceiver.
174
+ * No need to add/remove tracks, no renegotiation needed (transceiver already in SDP).
175
+ * Just swap the track and update direction if needed.
176
+ */
167
177
  startScreenShare(): Promise<MediaStream>;
168
- stopScreenShare(): void;
178
+ /**
179
+ * Stop screen sharing: clear the screen transceiver track and flip direction back to recvonly.
180
+ */
181
+ stopScreenShare(): Promise<void>;
169
182
  sendChatMessage(payload: ChatInput): void;
170
- disconnect(): void;
183
+ disconnect(): Promise<void>;
171
184
  private flushIce;
172
185
  private send;
173
186
  approveJoinRequest(requestId: string): void;
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,7 @@ var VideoSDKCore = class {
685
684
  }
686
685
  }
687
686
  }
688
- // ---------------- PEER ----------------
689
- createPeer(id) {
687
+ async createPeer(id) {
690
688
  if (!this.localStream) throw new Error("No local stream");
691
689
  if (!this.iceServers || this.iceServers.length === 0) {
692
690
  throw new Error(
@@ -694,7 +692,9 @@ var VideoSDKCore = class {
694
692
  );
695
693
  }
696
694
  console.log(
697
- "Adding tracks",
695
+ "Creating peer connection for",
696
+ id,
697
+ "with tracks:",
698
698
  this.localStream.getTracks().map((t) => ({
699
699
  kind: t.kind,
700
700
  enabled: t.enabled,
@@ -704,20 +704,52 @@ var VideoSDKCore = class {
704
704
  const pc = new RTCPeerConnection({
705
705
  iceServers: this.iceServers
706
706
  });
707
+ const audioTransceiver = pc.addTransceiver("audio", {
708
+ direction: "sendrecv"
709
+ });
710
+ const audioTrack = this.localStream.getAudioTracks()[0];
711
+ if (audioTrack) {
712
+ await audioTransceiver.sender.replaceTrack(audioTrack);
713
+ }
714
+ const cameraTransceiver = pc.addTransceiver("video", {
715
+ direction: "sendrecv"
716
+ });
717
+ const videoTrack = this.localStream.getVideoTracks()[0];
718
+ if (videoTrack) {
719
+ await cameraTransceiver.sender.replaceTrack(videoTrack);
720
+ }
721
+ const screenTransceiver = pc.addTransceiver("video", {
722
+ direction: this.isScreenSharing ? "sendrecv" : "recvonly"
723
+ });
724
+ if (this.isScreenSharing && this.screenStream) {
725
+ const screenTrack = this.screenStream.getVideoTracks()[0];
726
+ if (screenTrack) {
727
+ await screenTransceiver.sender.replaceTrack(screenTrack);
728
+ }
729
+ }
730
+ this.peerTransceivers[id] = {
731
+ cameraTransceiver,
732
+ screenTransceiver,
733
+ screenMid: null
734
+ // will be populated after negotiation
735
+ };
707
736
  pc.ontrack = (event) => {
737
+ const transceiver = event.transceiver;
738
+ const isScreenTrack = transceiver === this.peerTransceivers[id]?.screenTransceiver;
739
+ console.log(
740
+ `[ontrack] ${id}: kind=${event.track.kind}, mid=${transceiver.mid}, isScreen=${isScreenTrack}`
741
+ );
708
742
  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
743
  if (event.track.muted) {
712
744
  event.track.onunmute = () => {
713
- console.log(`${event.track.kind} track unmuted for ${id}`);
745
+ console.log(`[ontrack] ${event.track.kind} track unmuted for ${id}`);
714
746
  };
715
747
  }
716
- if (isScreenStream) {
717
- const videoTrack = event.track.kind === "video" ? event.track : incomingStream.getVideoTracks()[0] || participant?.media?.screenTrack;
748
+ if (isScreenTrack) {
749
+ const videoTrack2 = event.track.kind === "video" ? event.track : incomingStream.getVideoTracks()[0];
718
750
  this.state.updateParticipantMedia(id, {
719
751
  screenStream: incomingStream,
720
- screenTrack: videoTrack,
752
+ screenTrack: videoTrack2,
721
753
  isScreenSharing: true
722
754
  });
723
755
  if (!this.state.presenterId) {
@@ -743,29 +775,41 @@ var VideoSDKCore = class {
743
775
  });
744
776
  };
745
777
  pc.oniceconnectionstatechange = () => {
746
- console.log(`ICE Connection State: ${pc.iceConnectionState}`);
778
+ console.log(`[ICE Connection] ${id}: ${pc.iceConnectionState}`);
747
779
  };
748
780
  pc.onconnectionstatechange = () => {
781
+ console.log(`[Connection] ${id}: ${pc.connectionState}`);
749
782
  if (pc.connectionState === "failed") {
750
783
  try {
751
784
  pc.restartIce();
752
- } catch {
785
+ } catch (e) {
786
+ console.warn("Failed to restart ICE:", e);
753
787
  }
754
788
  }
755
789
  };
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
790
  return pc;
767
791
  }
768
- // ---------------- OFFER ----------------
792
+ /**
793
+ * Capture the screen transceiver's MID after SDP negotiation completes.
794
+ * The MID is assigned during negotiation and is stable for the life of the connection.
795
+ */
796
+ captureScreenMid(peerId) {
797
+ const pc = this.peers[peerId];
798
+ if (!pc) return;
799
+ const transceivers = pc.getTransceivers();
800
+ const screenTransceiver = this.peerTransceivers[peerId]?.screenTransceiver;
801
+ if (!screenTransceiver) return;
802
+ const negotiatedTransceiver = transceivers.find(
803
+ (t) => t === screenTransceiver
804
+ );
805
+ if (negotiatedTransceiver?.mid) {
806
+ this.peerTransceivers[peerId].screenMid = negotiatedTransceiver.mid;
807
+ console.log(
808
+ `[Negotiation] Captured screenMid for ${peerId}: ${negotiatedTransceiver.mid}`
809
+ );
810
+ }
811
+ }
812
+ // OFFER
769
813
  async createOffer(id, isRenegotiation = false) {
770
814
  if (!isRenegotiation && !this.shouldInitiate(id)) {
771
815
  console.debug(
@@ -783,12 +827,13 @@ var VideoSDKCore = class {
783
827
  this.initiators.add(id);
784
828
  }
785
829
  if (!this.peers[id]) {
786
- this.peers[id] = this.createPeer(id);
830
+ this.peers[id] = await this.createPeer(id);
787
831
  }
788
832
  const pc = this.peers[id];
789
833
  try {
790
834
  const offer = await pc.createOffer();
791
835
  await pc.setLocalDescription(offer);
836
+ this.captureScreenMid(id);
792
837
  this.send({
793
838
  type: "OFFER",
794
839
  payload: offer.sdp,
@@ -798,12 +843,18 @@ var VideoSDKCore = class {
798
843
  console.debug(`[Offer] Sent to ${id}`);
799
844
  } catch (err) {
800
845
  console.error(`[Offer] Failed for ${id}:`, err);
846
+ this.emitError(
847
+ "OFFER_CREATION_FAILED",
848
+ `Failed to create offer for ${id}`,
849
+ err,
850
+ true
851
+ );
801
852
  }
802
853
  }
803
854
  shouldInitiate(peerId) {
804
855
  return this.myId < peerId;
805
856
  }
806
- // ---------------- ANSWER ----------------
857
+ // ANSWER
807
858
  async handleOffer(sdp, id) {
808
859
  if (!this.iceServers || this.iceServers.length === 0) {
809
860
  console.warn("[Offer] Waiting for iceServers, queuing offer from", id);
@@ -811,7 +862,7 @@ var VideoSDKCore = class {
811
862
  return;
812
863
  }
813
864
  if (!this.peers[id]) {
814
- this.peers[id] = this.createPeer(id);
865
+ this.peers[id] = await this.createPeer(id);
815
866
  }
816
867
  const pc = this.peers[id];
817
868
  try {
@@ -827,8 +878,9 @@ var VideoSDKCore = class {
827
878
  );
828
879
  pc.close();
829
880
  delete this.peers[id];
881
+ delete this.peerTransceivers[id];
830
882
  this.initiators.delete(id);
831
- this.peers[id] = this.createPeer(id);
883
+ this.peers[id] = await this.createPeer(id);
832
884
  }
833
885
  }
834
886
  if (this.peers[id].signalingState !== "stable" && this.peers[id].signalingState !== "have-local-offer") {
@@ -841,6 +893,7 @@ var VideoSDKCore = class {
841
893
  type: "offer",
842
894
  sdp
843
895
  });
896
+ this.captureScreenMid(id);
844
897
  const pending = this.pendingIceCandidates[id] || [];
845
898
  for (const candidate of pending) {
846
899
  try {
@@ -870,18 +923,26 @@ var VideoSDKCore = class {
870
923
  );
871
924
  }
872
925
  }
873
- // ---------------- CLEANUP ----------------
926
+ // CLEANUP
874
927
  closePeer(id) {
875
928
  const pc = this.peers[id];
876
929
  if (!pc) return;
877
930
  pc.ontrack = null;
878
931
  pc.onicecandidate = null;
879
932
  pc.onconnectionstatechange = null;
933
+ pc.oniceconnectionstatechange = null;
880
934
  pc.close();
881
935
  delete this.peers[id];
936
+ delete this.peerTransceivers[id];
882
937
  this.initiators.delete(id);
883
938
  this.state.removeParticipant(id);
884
939
  }
940
+ // SCREEN SHARE (TRANSCEIVER-BASED)
941
+ /**
942
+ * Start screen sharing using replaceTrack on the pre-established screen transceiver.
943
+ * No need to add/remove tracks, no renegotiation needed (transceiver already in SDP).
944
+ * Just swap the track and update direction if needed.
945
+ */
885
946
  async startScreenShare() {
886
947
  try {
887
948
  if (this.state.presenterId && this.state.presenterId !== this.myId) {
@@ -892,34 +953,55 @@ var VideoSDKCore = class {
892
953
  }
893
954
  this.screenStream = await navigator.mediaDevices.getDisplayMedia({
894
955
  video: true
895
- // audio: true,
896
956
  });
957
+ const screenTrack = this.screenStream.getVideoTracks()[0];
958
+ if (!screenTrack) {
959
+ throw new Error("No video track in screen stream");
960
+ }
897
961
  this.isScreenSharing = true;
898
962
  this.state.updateLocalParticipant({
899
963
  media: {
900
964
  isScreenSharing: true,
901
965
  screenStream: this.screenStream,
902
- screenTrack: this.screenStream.getVideoTracks()[0]
966
+ screenTrack
903
967
  }
904
968
  });
905
969
  this.state.setPresenterId(this.myId);
906
- this.screenStream.getVideoTracks()[0].onended = () => {
970
+ screenTrack.onended = () => {
971
+ console.log("[Screen Share] User stopped via browser button");
907
972
  this.stopScreenShare();
908
973
  };
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
- });
974
+ for (const [peerId, pc] of Object.entries(this.peers)) {
975
+ const txInfo = this.peerTransceivers[peerId];
976
+ if (!txInfo) {
977
+ console.warn(
978
+ `[Screen Share] No transceiver info for ${peerId}, skipping`
979
+ );
980
+ continue;
981
+ }
982
+ try {
983
+ await txInfo.screenTransceiver.sender.replaceTrack(screenTrack);
984
+ if (txInfo.screenTransceiver.currentDirection === "recvonly") {
985
+ txInfo.screenTransceiver.direction = "sendrecv";
986
+ console.log(
987
+ `[Screen Share] Flipped ${peerId} screen transceiver to sendrecv`
988
+ );
989
+ await this.createOffer(peerId, true);
990
+ }
991
+ } catch (err) {
992
+ console.error(
993
+ `[Screen Share] Failed to update transceiver for ${peerId}:`,
994
+ err
995
+ );
996
+ }
997
+ }
917
998
  this.send({
918
999
  type: "SCREEN_SHARE_START",
919
1000
  sender: this.myId,
920
1001
  room_id: this.room.id,
921
1002
  stream_id: this.screenStream.id.replace(/[{}]/g, "")
922
1003
  });
1004
+ console.log("[Screen Share] Started successfully");
923
1005
  return this.screenStream;
924
1006
  } catch (err) {
925
1007
  this.emitError(
@@ -933,21 +1015,32 @@ var VideoSDKCore = class {
933
1015
  throw err;
934
1016
  }
935
1017
  }
936
- stopScreenShare() {
1018
+ /**
1019
+ * Stop screen sharing: clear the screen transceiver track and flip direction back to recvonly.
1020
+ */
1021
+ async stopScreenShare() {
937
1022
  if (!this.screenStream) return;
1023
+ console.log("[Screen Share] Stopping...");
938
1024
  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);
1025
+ for (const [peerId, pc] of Object.entries(this.peers)) {
1026
+ const txInfo = this.peerTransceivers[peerId];
1027
+ if (!txInfo) continue;
1028
+ try {
1029
+ await txInfo.screenTransceiver.sender.replaceTrack(null);
1030
+ if (txInfo.screenTransceiver.currentDirection === "sendrecv") {
1031
+ txInfo.screenTransceiver.direction = "recvonly";
1032
+ console.log(
1033
+ `[Screen Share] Flipped ${peerId} screen transceiver to recvonly`
1034
+ );
1035
+ await this.createOffer(peerId, true);
946
1036
  }
947
- });
948
- delete this.screenSenders[peerId];
949
- this.createOffer(peerId, true);
950
- });
1037
+ } catch (err) {
1038
+ console.error(
1039
+ `[Screen Share] Failed to clear transceiver for ${peerId}:`,
1040
+ err
1041
+ );
1042
+ }
1043
+ }
951
1044
  this.screenStream = null;
952
1045
  this.isScreenSharing = false;
953
1046
  this.state.updateLocalParticipant({
@@ -965,7 +1058,9 @@ var VideoSDKCore = class {
965
1058
  sender: this.myId,
966
1059
  room_id: this.room.id
967
1060
  });
1061
+ console.log("[Screen Share] Stopped");
968
1062
  }
1063
+ // CHAT
969
1064
  sendChatMessage(payload) {
970
1065
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
971
1066
  console.warn("WS not connected");
@@ -998,11 +1093,13 @@ var VideoSDKCore = class {
998
1093
  client_ts: Date.now()
999
1094
  });
1000
1095
  }
1001
- disconnect() {
1096
+ // DISCONNECT
1097
+ async disconnect() {
1002
1098
  this.intentionalDisconnect = true;
1003
- this.stopScreenShare();
1099
+ await this.stopScreenShare();
1004
1100
  Object.values(this.peers).forEach((pc) => pc.close());
1005
1101
  this.peers = {};
1102
+ this.peerTransceivers = {};
1006
1103
  this.initiators.clear();
1007
1104
  this.stopHeartbeat();
1008
1105
  if (this.ws?.readyState === WebSocket.OPEN) {